First step towards single tokenization.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Mon, 23 Mar 2015 20:36:07 +0000 (20:36 +0000)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Sun, 12 Apr 2015 11:15:29 +0000 (12:15 +0100)
* Removes tokenization from all steps in favour to single at the beginning.
* Adds tokenization of properties value by value instead of all in one piece, e.g.
  `margin:0px 1px` gets tokenized into `margin`, `0px, and `1px`.
* Full tokenization means more detailed source maps too.
* Reworks override compactor and associated classes to use single tokenization.
* Simplifies override compactor code so it's much easier to understand.
* Adds loads of tests to override compactor and family.

Caveats / in progress:
* Shorthand compactor is turned off.
* Inherit compacting is gone (but will be back).
* Some multivalue background tests are failing.
* There's no way to turn off compacting (possible performance regression).

49 files changed:
lib/properties/break-up.js [new file with mode: 0644]
lib/properties/can-override.js [new file with mode: 0644]
lib/properties/compactable.js [new file with mode: 0644]
lib/properties/optimizer.js
lib/properties/override-compactor.js
lib/properties/populate-components.js [new file with mode: 0644]
lib/properties/processable.js [deleted file]
lib/properties/remove-unused.js [new file with mode: 0644]
lib/properties/restore-shorthands.js [new file with mode: 0644]
lib/properties/restore.js [new file with mode: 0644]
lib/properties/shallow-clone.js [new file with mode: 0644]
lib/properties/shorthand-compactor.js
lib/properties/token.js [deleted file]
lib/properties/validator.js
lib/properties/wrap-for-optimizing.js [new file with mode: 0644]
lib/selectors/extractor.js [moved from lib/properties/extractor.js with 71% similarity]
lib/selectors/metadata.js [new file with mode: 0644]
lib/selectors/optimization-metadata.js [new file with mode: 0644]
lib/selectors/optimizer.js
lib/selectors/optimizers/advanced.js
lib/selectors/optimizers/simple.js
lib/selectors/reorderable.js [moved from lib/properties/reorderable.js with 88% similarity]
lib/selectors/source-map-stringifier.js
lib/selectors/stringifier.js
lib/utils/extractors.js
lib/utils/splitter.js
lib/utils/stringify-tokens.js
test/binary-test.js
test/integration-test.js
test/properties/break-up-test.js [new file with mode: 0644]
test/properties/longhand-overriding-test.js [new file with mode: 0644]
test/properties/optimizer-test.js [new file with mode: 0644]
test/properties/override-compacting-test.js [new file with mode: 0644]
test/properties/populate-components-test.js [new file with mode: 0644]
test/properties/remove-unused-test.js [new file with mode: 0644]
test/properties/reorderable-test.js [deleted file]
test/properties/restore-shorthands-test.js [new file with mode: 0644]
test/properties/restore-test.js [new file with mode: 0644]
test/properties/shorthand-compacting-test.js [new file with mode: 0644]
test/properties/wrap-for-optimizing-test.js [new file with mode: 0644]
test/selectors/extractor-test.js [moved from test/properties/extractor-test.js with 56% similarity]
test/selectors/optimization-metadata-test.js [new file with mode: 0644]
test/selectors/optimizer-test.js
test/selectors/optimizers/simple-test.js
test/selectors/reorderable-test.js [new file with mode: 0644]
test/selectors/tokenizer-source-maps-test.js
test/selectors/tokenizer-test.js
test/source-map-test.js
test/utils/splitter-test.js

diff --git a/lib/properties/break-up.js b/lib/properties/break-up.js
new file mode 100644 (file)
index 0000000..e585d9a
--- /dev/null
@@ -0,0 +1,306 @@
+var wrapSingle = require('./wrap-for-optimizing').single;
+var validator = require('./validator');
+
+var Splitter = require('../utils/splitter');
+
+function _colorFilter(value) {
+  return value[0] == 'invert' || validator.isValidColor(value[0]);
+}
+
+function _styleFilter(value) {
+  return value[0] != 'inherit' && validator.isValidStyle(value[0]);
+}
+
+function _wrapDefault(name, property, compactable) {
+  var descriptor = compactable[name];
+  if (descriptor.doubleValues && descriptor.defaultValue.length == 2)
+    return wrapSingle([[name, property.important], [descriptor.defaultValue[0]], [descriptor.defaultValue[1]]]);
+  else if (descriptor.doubleValues && descriptor.defaultValue.length == 1)
+    return wrapSingle([[name, property.important], [descriptor.defaultValue[0]]]);
+  else
+    return wrapSingle([[name, property.important], [descriptor.defaultValue]]);
+}
+
+function _widthFilter(value) {
+  return value[0] != 'inherit' && validator.isValidWidth(value[0]);
+}
+
+function background(property, compactable) {
+  var image = _wrapDefault('background-image', property, compactable);
+  var position = _wrapDefault('background-position', property, compactable);
+  var size = _wrapDefault('background-size', property, compactable);
+  var repeat = _wrapDefault('background-repeat', property, compactable);
+  var attachment = _wrapDefault('background-attachment', property, compactable);
+  var origin = _wrapDefault('background-origin', property, compactable);
+  var clip = _wrapDefault('background-clip', property, compactable);
+  var color = _wrapDefault('background-color', property, compactable);
+  var components = [image, position, size, repeat, attachment, origin, clip, color];
+  var values = property.value;
+
+  var positionSet = false;
+  var clipSet = false;
+  var originSet = false;
+  var repeatSet = false;
+
+  if (property.value.length == 1 && property.value[0][0] == 'inherit') {
+    // NOTE: 'inherit' is not a valid value for background-attachment
+    color.value = image.value =  repeat.value = position.value = size.value = origin.value = clip.value = property.value;
+    return components;
+  }
+
+  for (var i = values.length - 1; i >= 0; i--) {
+    var value = values[i];
+
+    if (validator.isValidBackgroundAttachment(value[0])) {
+      attachment.value = [value];
+    } else if (validator.isValidBackgroundBox(value[0])) {
+      if (clipSet) {
+        origin.value = [value];
+        originSet = true;
+      } else {
+        clip.value = [value];
+        clipSet = true;
+      }
+    } else if (validator.isValidBackgroundRepeat(value[0])) {
+      if (repeatSet) {
+        repeat.value.unshift(value);
+      } else {
+        repeat.value = [value];
+        repeatSet = true;
+      }
+    } else if (validator.isValidBackgroundPositionPart(value[0]) || validator.isValidBackgroundSizePart(value[0])) {
+      if (i > 0) {
+        var previousValue = values[i - 1];
+
+        if (previousValue[0].indexOf('/') > 0) {
+          var twoParts = new Splitter('/').split(previousValue[0]);
+          // NOTE: we do this slicing as value may contain metadata too, like for source maps
+          size.value = [[twoParts.pop()].concat(previousValue.slice(1)), value];
+          values[i - 1] = [twoParts.pop()].concat(previousValue.slice(1));
+        } else if (i > 1 && values[i - 2] == '/') {
+          size.value = [previousValue, value];
+          i -= 2;
+        } else if (values[i - 1] == '/') {
+          size.value = [value];
+        } else {
+          if (!positionSet)
+            position.value = [];
+
+          position.value.unshift(value);
+          positionSet = true;
+        }
+      } else {
+        if (!positionSet)
+          position.value = [];
+
+        position.value.unshift(value);
+        positionSet = true;
+      }
+    } else if (validator.isValidBackgroundPositionAndSize(value[0])) {
+      var sizeValue = new Splitter('/').split(value[0]);
+      // NOTE: we do this slicing as value may contain metadata too, like for source maps
+      size.value = [[sizeValue.pop()].concat(value.slice(1))];
+      position.value = [[sizeValue.pop()].concat(value.slice(1))];
+    } else if ((color.value == compactable[color.name].defaultValue || color.value == 'none') && validator.isValidColor(value[0])) {
+      color.value = [value];
+    } else if (validator.isValidUrl(value[0]) || validator.isValidFunction(value[0])) {
+      image.value = [value];
+    }
+  }
+
+  if (clipSet && !originSet)
+    origin.value = clip.value;
+
+  return components;
+}
+
+function borderRadius(property, compactable) {
+  var values = property.value;
+  var splitAt = -1;
+
+  for (var i = 0, l = values.length; i < l; i++) {
+    if (values[i][0] == '/') {
+      splitAt = i;
+      break;
+    }
+  }
+
+  if (splitAt == -1)
+    return fourValues(property, compactable);
+
+  var target = _wrapDefault(property.name, property, compactable);
+  target.value = values.slice(0, splitAt);
+  target.components = fourValues(target, compactable);
+
+  var remainder = _wrapDefault(property.name, property, compactable);
+  remainder.value = values.slice(splitAt + 1);
+  remainder.components = fourValues(remainder, compactable);
+
+  for (var j = 0; j < 4; j++) {
+    target.components[j].value = [target.components[j].value, remainder.components[j].value];
+  }
+
+  return target.components;
+}
+
+function fourValues(property, compactable) {
+  var componentNames = compactable[property.name].components;
+  var components = [];
+  var value = property.value;
+
+  if (value.length < 1)
+    return [];
+
+  if (value.length < 2)
+    value[1] = value[0];
+  if (value.length < 3)
+    value[2] = value[0];
+  if (value.length < 4)
+    value[3] = value[1];
+
+  for (var i = componentNames.length - 1; i >= 0; i--) {
+    var component = wrapSingle([[componentNames[i], property.important]]);
+    component.value = [value[i]];
+    components.unshift(component);
+  }
+
+  return components;
+}
+
+function multipleValues(splitWith) {
+  return function (property, compactable) {
+    var splitsAt = [];
+    var values = property.value;
+    var i, j, l, m;
+
+    // find split commas
+    for (i = 0, l = values.length; i < l; i++) {
+      if (values[i][0] == ',')
+        splitsAt.push(i);
+    }
+
+    if (splitsAt.length === 0)
+      return splitWith(property, compactable);
+
+    var splitComponents = [];
+
+    // split over commas, and into components
+    for (i = 0, l = splitsAt.length; i <= l; i++) {
+      var from = i === 0 ? 0 : splitsAt[i - 1] + 1;
+      var to = i < l ? splitsAt[i] : values.length;
+
+      var _property = _wrapDefault(property.name, property, compactable);
+      _property.value = values.slice(from, to);
+
+      splitComponents.push(splitWith(_property, compactable));
+    }
+
+    var components = splitComponents[0];
+
+    // group component values from each split
+    for (i = 0, l = components.length; i < l; i++) {
+      components[i].value = [components[i].value];
+      components[i].multiplex = true;
+
+      for (j = 1, m = splitComponents.length; j < m; j++) {
+        components[i].value.push(splitComponents[j][i].value);
+      }
+    }
+
+    return components;
+  };
+}
+
+function listStyle(property, compactable) {
+  var type = _wrapDefault('list-style-type', property, compactable);
+  var position = _wrapDefault('list-style-position', property, compactable);
+  var image = _wrapDefault('list-style-image', property, compactable);
+  var components = [type, position, image];
+
+  if (property.value.length == 1 && property.value[0][0] == 'inherit') {
+    type.value = position.value = image.value = [property.value[0]];
+    return components;
+  }
+
+  var values = property.value;
+  var index = 0;
+
+  if (index < values.length && validator.isValidListStyleType(values[index][0]))
+    type.value = [values[index++]];
+  if (index < values.length && validator.isValidListStylePosition(values[index][0]))
+    position.value = [values[index++]];
+  if (index < values.length)
+    image.value = [values[index]];
+
+  return components;
+}
+
+function widthStyleColor(property, compactable) {
+  var descriptor = compactable[property.name];
+  var components = [
+    _wrapDefault(descriptor.components[0], property, compactable),
+    _wrapDefault(descriptor.components[1], property, compactable),
+    _wrapDefault(descriptor.components[2], property, compactable)
+  ];
+  var color, style, width;
+
+  for (var i = 0; i < 3; i++) {
+    var component = components[i];
+
+    if (component.name.indexOf('color') > 0)
+      color = component;
+    else if (component.name.indexOf('style') > 0)
+      style = component;
+    else
+      width = component;
+  }
+
+  if ((property.value.length == 1 && property.value[0][0] == 'inherit') ||
+      (property.value.length == 3 && property.value[0][0] == 'inherit' && property.value[1][0] == 'inherit' && property.value[2][0] == 'inherit')) {
+    color.value = style.value = width.value = [property.value[0]];
+    return components;
+  }
+
+  var values = property.value.slice(0);
+  var match, matches;
+
+  // 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
+
+  if (values.length > 0) {
+    matches = values.filter(_widthFilter);
+    match = matches.length > 1 && matches[0] == 'none' ? matches[1] : matches[0];
+    if (match) {
+      width.value = [match];
+      values.splice(values.indexOf(match), 1);
+    }
+  }
+
+  if (values.length > 0) {
+    match = values.filter(_styleFilter)[0];
+    if (match) {
+      style.value = [match];
+      values.splice(values.indexOf(match), 1);
+    }
+  }
+
+  if (values.length > 0) {
+    match = values.filter(_colorFilter)[0];
+    if (match) {
+      color.value = [match];
+      values.splice(values.indexOf(match), 1);
+    }
+  }
+
+  return components;
+}
+
+module.exports = {
+  background: background,
+  border: widthStyleColor,
+  borderRadius: borderRadius,
+  fourValues: fourValues,
+  listStyle: listStyle,
+  multipleValues: multipleValues,
+  outline: widthStyleColor
+};
diff --git a/lib/properties/can-override.js b/lib/properties/can-override.js
new file mode 100644 (file)
index 0000000..ffccfd2
--- /dev/null
@@ -0,0 +1,126 @@
+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.
+
+function _valueOf(property) {
+  return Array.isArray(property.value[0]) ? property.value[0][0] : property.value[0];
+}
+
+// Use when two tokens of the same property can always be merged
+function always() {
+  return true;
+}
+
+function backgroundImage(property1, property2) {
+  // 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)
+  var image1 = _valueOf(property1);
+  var image2 = _valueOf(property2);
+
+  if (image2 == 'none' || image2 == 'inherit' || validator.isValidUrl(image2))
+    return true;
+  if (image1 == 'none' || image1 == 'inherit' || validator.isValidUrl(image1))
+    return false;
+
+  // Functions with the same name can override each other; same values can override each other
+  return sameFunctionOrValue(property1, property2);
+}
+
+function border(property1, property2) {
+  return color(property1.components[2], property2.components[2]);
+}
+
+// Use for color properties (color, background-color, border-color, etc.)
+function color(property1, property2) {
+  // 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
+
+  var color1 = _valueOf(property1);
+  var color2 = _valueOf(property2);
+
+  // (hex | named)
+  if (validator.isValidNamedColor(color2) || validator.isValidHexColor(color2))
+    return true;
+  if (validator.isValidNamedColor(color1) || validator.isValidHexColor(color1))
+    return false;
+
+  // (rgba|hsla)
+  if (validator.isValidRgbaColor(color2) || validator.isValidHslaColor(color2))
+    return true;
+  if (validator.isValidRgbaColor(color1) || validator.isValidHslaColor(color1))
+    return false;
+
+  // Functions with the same name can override each other; same values can override each other
+  return sameFunctionOrValue(property1, property2);
+}
+
+function twoOptionalFunctions(property1, property2) {
+  var value1 = _valueOf(property1);
+  var value2 = _valueOf(property2);
+
+  return !(validator.isValidFunction(value1) ^ validator.isValidFunction(value2));
+}
+
+function sameValue(property1, property2) {
+  var value1 = _valueOf(property1);
+  var value2 = _valueOf(property2);
+
+  return value1 === value2;
+}
+
+function sameFunctionOrValue(property1, property2) {
+  var value1 = _valueOf(property1);
+  var value2 = _valueOf(property2);
+
+  // Functions with the same name can override each other
+  if (validator.areSameFunction(value1, value2))
+    return true;
+
+  return value1 === value2;
+}
+
+// Use for properties containing CSS units (margin-top, padding-left, etc.)
+function unit(property1, property2) {
+  // 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
+  var value1 = _valueOf(property1);
+  var value2 = _valueOf(property2);
+
+  if (validator.isValidAndCompatibleUnitWithoutFunction(value1) && !validator.isValidAndCompatibleUnitWithoutFunction(value2))
+    return false;
+
+  if (validator.isValidUnitWithoutFunction(value2))
+    return true;
+  if (validator.isValidUnitWithoutFunction(value1))
+    return false;
+
+  // Standard non-vendor-prefixed functions can override each other
+  if (validator.isValidFunctionWithoutVendorPrefix(value2) && validator.isValidFunctionWithoutVendorPrefix(value1)) {
+    return true;
+  }
+
+  // Functions with the same name can override each other; same values can override each other
+  return sameFunctionOrValue(property1, property2);
+}
+
+module.exports = {
+  always: always,
+  backgroundImage: backgroundImage,
+  border: border,
+  color: color,
+  sameValue: sameValue,
+  sameFunctionOrValue: sameFunctionOrValue,
+  twoOptionalFunctions: twoOptionalFunctions,
+  unit: unit
+};
diff --git a/lib/properties/compactable.js b/lib/properties/compactable.js
new file mode 100644 (file)
index 0000000..47bc0a3
--- /dev/null
@@ -0,0 +1,275 @@
+// Contains the interpretation of CSS properties, as used by the property optimizer
+
+var breakUp = require('./break-up');
+var canOverride = require('./can-override');
+var restore = require('./restore');
+
+// 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.
+//
+// * restore: function (Only specify for shorthand properties.)
+//   Puts the shorthand together from its components.
+//
+var compactable = {
+  'color': {
+    canOverride: canOverride.color,
+    defaultValue: 'transparent',
+    shortestValue: 'red'
+  },
+  'background': {
+    components: [
+      'background-image',
+      'background-position',
+      'background-size',
+      'background-repeat',
+      'background-attachment',
+      'background-origin',
+      'background-clip',
+      'background-color'
+    ],
+    breakUp: breakUp.multipleValues(breakUp.background),
+    defaultValue: '0 0',
+    restore: restore.multipleValues(restore.background),
+    shortestValue: '0',
+    shorthand: true
+  },
+  'background-clip': {
+    canOverride: canOverride.always,
+    defaultValue: 'border-box',
+    shortestValue: 'border-box'
+  },
+  'background-color': {
+    canOverride: canOverride.color,
+    defaultValue: 'transparent',
+    multiplexLastOnly: true,
+    nonMergeableValue: 'none',
+    shortestValue: 'red'
+  },
+  'background-image': {
+    canOverride: canOverride.backgroundImage,
+    defaultValue: 'none'
+  },
+  'background-origin': {
+    canOverride: canOverride.always,
+    defaultValue: 'padding-box',
+    shortestValue: 'border-box'
+  },
+  'background-repeat': {
+    canOverride: canOverride.always,
+    defaultValue: ['repeat'],
+    doubleValues: true
+  },
+  'background-position': {
+    canOverride: canOverride.always,
+    defaultValue: ['0', '0'],
+    doubleValues: true,
+    shortestValue: '0'
+  },
+  'background-size': {
+    canOverride: canOverride.always,
+    defaultValue: ['auto'],
+    doubleValues: true,
+    shortestValue: '0 0'
+  },
+  'background-attachment': {
+    canOverride: canOverride.always,
+    defaultValue: 'scroll'
+  },
+  'border': {
+    breakUp: breakUp.border,
+    canOverride: canOverride.border,
+    components: [
+      'border-width',
+      'border-style',
+      'border-color'
+    ],
+    defaultValue: 'none',
+    restore: restore.withoutDefaults,
+    shorthand: true
+  },
+  'border-color': {
+    canOverride: canOverride.color,
+    defaultValue: 'none',
+    shorthand: true
+  },
+  'border-style': {
+    canOverride: canOverride.always,
+    defaultValue: 'none',
+    shorthand: true
+  },
+  'border-width': {
+    canOverride: canOverride.unit,
+    defaultValue: 'medium',
+    shortestValue: '0',
+    shorthand: true
+  },
+  'list-style': {
+    components: [
+      'list-style-type',
+      'list-style-position',
+      'list-style-image'
+    ],
+    canOverride: canOverride.always,
+    breakUp: breakUp.listStyle,
+    restore: restore.withoutDefaults,
+    defaultValue: 'outside', // can't use 'disc' because that'd override default 'decimal' for <ol>
+    shortestValue: 'none',
+    shorthand: true
+  },
+  'list-style-type' : {
+    canOverride: canOverride.always,
+    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
+    shortestValue: 'none'
+  },
+  '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,
+    restore: restore.withoutDefaults,
+    defaultValue: '0',
+    shorthand: true
+  },
+  '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'
+  },
+  '-moz-transform': {
+    canOverride: canOverride.sameFunctionOrValue
+  },
+  '-ms-transform': {
+    canOverride: canOverride.sameFunctionOrValue
+  },
+  '-webkit-transform': {
+    canOverride: canOverride.sameFunctionOrValue
+  },
+  'transform': {
+    canOverride: canOverride.sameFunctionOrValue
+  }
+};
+
+var addFourValueShorthand = function (prop, components, options) {
+  options = options || {};
+  compactable[prop] = {
+    components: components,
+    breakUp: options.breakUp || breakUp.fourValues,
+    defaultValue: options.defaultValue || '0',
+    restore: options.restore || restore.fourValues,
+    shortestValue: options.shortestValue,
+    shorthand: true
+  };
+  for (var i = 0; i < components.length; i++) {
+    compactable[components[i]] = {
+      breakUp: options.breakUp || breakUp.fourValues,
+      canOverride: options.canOverride || canOverride.unit,
+      defaultValue: options.defaultValue || '0',
+      shortestValue: options.shortestValue
+    };
+  }
+};
+
+['', '-moz-', '-o-', '-webkit-'].forEach(function (prefix) {
+  addFourValueShorthand(prefix + 'border-radius', [
+    prefix + 'border-top-left-radius',
+    prefix + 'border-top-right-radius',
+    prefix + 'border-bottom-right-radius',
+    prefix + 'border-bottom-left-radius'
+  ], {
+    breakUp: breakUp.borderRadius,
+    restore: restore.borderRadius
+  });
+});
+
+addFourValueShorthand('border-color', [
+  'border-top-color',
+  'border-right-color',
+  'border-bottom-color',
+  'border-left-color'
+], {
+  breakUp: breakUp.fourValues,
+  canOverride: canOverride.color,
+  defaultValue: 'none',
+  shortestValue: 'red'
+});
+
+addFourValueShorthand('border-style', [
+  'border-top-style',
+  'border-right-style',
+  'border-bottom-style',
+  'border-left-style'
+], {
+  breakUp: breakUp.fourValues,
+  canOverride: canOverride.always,
+  defaultValue: 'none'
+});
+
+addFourValueShorthand('border-width', [
+  'border-top-width',
+  'border-right-width',
+  'border-bottom-width',
+  'border-left-width'
+], {
+  defaultValue: 'medium',
+  shortestValue: '0'
+});
+
+addFourValueShorthand('padding', [
+  'padding-top',
+  'padding-right',
+  'padding-bottom',
+  'padding-left'
+]);
+
+addFourValueShorthand('margin', [
+  'margin-top',
+  'margin-right',
+  'margin-bottom',
+  'margin-left'
+]);
+
+module.exports = compactable;
index ce891f9..5bc75b3 100644 (file)
+var compactable = require('./compactable');
+var wrapForOptimizing = require('./wrap-for-optimizing').all;
+var populateComponents = require('./populate-components');
+var compactOverrides = require('./override-compactor');
+// var compactShorthands = require('./shorthand-compactor');
+var removeUnused = require('./remove-unused');
+var restoreShorthands = require('./restore-shorthands');
+
+var shorthands = {
+  'animation-delay': ['animation'],
+  'animation-direction': ['animation'],
+  'animation-duration': ['animation'],
+  'animation-fill-mode': ['animation'],
+  'animation-iteration-count': ['animation'],
+  'animation-name': ['animation'],
+  'animation-play-state': ['animation'],
+  'animation-timing-function': ['animation'],
+  '-moz-animation-delay': ['-moz-animation'],
+  '-moz-animation-direction': ['-moz-animation'],
+  '-moz-animation-duration': ['-moz-animation'],
+  '-moz-animation-fill-mode': ['-moz-animation'],
+  '-moz-animation-iteration-count': ['-moz-animation'],
+  '-moz-animation-name': ['-moz-animation'],
+  '-moz-animation-play-state': ['-moz-animation'],
+  '-moz-animation-timing-function': ['-moz-animation'],
+  '-o-animation-delay': ['-o-animation'],
+  '-o-animation-direction': ['-o-animation'],
+  '-o-animation-duration': ['-o-animation'],
+  '-o-animation-fill-mode': ['-o-animation'],
+  '-o-animation-iteration-count': ['-o-animation'],
+  '-o-animation-name': ['-o-animation'],
+  '-o-animation-play-state': ['-o-animation'],
+  '-o-animation-timing-function': ['-o-animation'],
+  '-webkit-animation-delay': ['-webkit-animation'],
+  '-webkit-animation-direction': ['-webkit-animation'],
+  '-webkit-animation-duration': ['-webkit-animation'],
+  '-webkit-animation-fill-mode': ['-webkit-animation'],
+  '-webkit-animation-iteration-count': ['-webkit-animation'],
+  '-webkit-animation-name': ['-webkit-animation'],
+  '-webkit-animation-play-state': ['-webkit-animation'],
+  '-webkit-animation-timing-function': ['-webkit-animation'],
+  'border-color': ['border'],
+  'border-style': ['border'],
+  'border-width': ['border'],
+  'border-bottom': ['border'],
+  'border-bottom-color': ['border-bottom', 'border-color', 'border'],
+  'border-bottom-style': ['border-bottom', 'border-style', 'border'],
+  'border-bottom-width': ['border-bottom', 'border-width', 'border'],
+  'border-left': ['border'],
+  'border-left-color': ['border-left', 'border-color', 'border'],
+  'border-left-style': ['border-left', 'border-style', 'border'],
+  'border-left-width': ['border-left', 'border-width', 'border'],
+  'border-right': ['border'],
+  'border-right-color': ['border-right', 'border-color', 'border'],
+  'border-right-style': ['border-right', 'border-style', 'border'],
+  'border-right-width': ['border-right', 'border-width', 'border'],
+  'border-top': ['border'],
+  'border-top-color': ['border-top', 'border-color', 'border'],
+  'border-top-style': ['border-top', 'border-style', 'border'],
+  'border-top-width': ['border-top', 'border-width', 'border'],
+  'font-family': ['font'],
+  'font-size': ['font'],
+  'font-style': ['font'],
+  'font-variant': ['font'],
+  'font-weight': ['font'],
+  'transition-delay': ['transition'],
+  'transition-duration': ['transition'],
+  'transition-property': ['transition'],
+  'transition-timing-function': ['transition'],
+  '-moz-transition-delay': ['-moz-transition'],
+  '-moz-transition-duration': ['-moz-transition'],
+  '-moz-transition-property': ['-moz-transition'],
+  '-moz-transition-timing-function': ['-moz-transition'],
+  '-o-transition-delay': ['-o-transition'],
+  '-o-transition-duration': ['-o-transition'],
+  '-o-transition-property': ['-o-transition'],
+  '-o-transition-timing-function': ['-o-transition'],
+  '-webkit-transition-delay': ['-webkit-transition'],
+  '-webkit-transition-duration': ['-webkit-transition'],
+  '-webkit-transition-property': ['-webkit-transition'],
+  '-webkit-transition-timing-function': ['-webkit-transition']
+};
 
-var processableInfo = require('./processable');
-var overrideCompactor = require('./override-compactor');
-var shorthandCompactor = require('./shorthand-compactor');
-
-module.exports = function Optimizer(options, context) {
-  var overridable = {
-    'animation-delay': ['animation'],
-    'animation-direction': ['animation'],
-    'animation-duration': ['animation'],
-    'animation-fill-mode': ['animation'],
-    'animation-iteration-count': ['animation'],
-    'animation-name': ['animation'],
-    'animation-play-state': ['animation'],
-    'animation-timing-function': ['animation'],
-    '-moz-animation-delay': ['-moz-animation'],
-    '-moz-animation-direction': ['-moz-animation'],
-    '-moz-animation-duration': ['-moz-animation'],
-    '-moz-animation-fill-mode': ['-moz-animation'],
-    '-moz-animation-iteration-count': ['-moz-animation'],
-    '-moz-animation-name': ['-moz-animation'],
-    '-moz-animation-play-state': ['-moz-animation'],
-    '-moz-animation-timing-function': ['-moz-animation'],
-    '-o-animation-delay': ['-o-animation'],
-    '-o-animation-direction': ['-o-animation'],
-    '-o-animation-duration': ['-o-animation'],
-    '-o-animation-fill-mode': ['-o-animation'],
-    '-o-animation-iteration-count': ['-o-animation'],
-    '-o-animation-name': ['-o-animation'],
-    '-o-animation-play-state': ['-o-animation'],
-    '-o-animation-timing-function': ['-o-animation'],
-    '-webkit-animation-delay': ['-webkit-animation'],
-    '-webkit-animation-direction': ['-webkit-animation'],
-    '-webkit-animation-duration': ['-webkit-animation'],
-    '-webkit-animation-fill-mode': ['-webkit-animation'],
-    '-webkit-animation-iteration-count': ['-webkit-animation'],
-    '-webkit-animation-name': ['-webkit-animation'],
-    '-webkit-animation-play-state': ['-webkit-animation'],
-    '-webkit-animation-timing-function': ['-webkit-animation'],
-    'background-clip': ['background'],
-    'background-origin': ['background'],
-    'border-color': ['border'],
-    'border-style': ['border'],
-    'border-width': ['border'],
-    'border-bottom': ['border'],
-    'border-bottom-color': ['border-bottom', 'border-color', 'border'],
-    'border-bottom-style': ['border-bottom', 'border-style', 'border'],
-    'border-bottom-width': ['border-bottom', 'border-width', 'border'],
-    'border-left': ['border'],
-    'border-left-color': ['border-left', 'border-color', 'border'],
-    'border-left-style': ['border-left', 'border-style', 'border'],
-    'border-left-width': ['border-left', 'border-width', 'border'],
-    'border-right': ['border'],
-    'border-right-color': ['border-right', 'border-color', 'border'],
-    'border-right-style': ['border-right', 'border-style', 'border'],
-    'border-right-width': ['border-right', 'border-width', 'border'],
-    'border-top': ['border'],
-    'border-top-color': ['border-top', 'border-color', 'border'],
-    'border-top-style': ['border-top', 'border-style', 'border'],
-    'border-top-width': ['border-top', 'border-width', 'border'],
-    'font-family': ['font'],
-    'font-size': ['font'],
-    'font-style': ['font'],
-    'font-variant': ['font'],
-    'font-weight': ['font'],
-    'margin-bottom': ['margin'],
-    'margin-left': ['margin'],
-    'margin-right': ['margin'],
-    'margin-top': ['margin'],
-    'padding-bottom': ['padding'],
-    'padding-left': ['padding'],
-    'padding-right': ['padding'],
-    'padding-top': ['padding'],
-    'transition-delay': ['transition'],
-    'transition-duration': ['transition'],
-    'transition-property': ['transition'],
-    'transition-timing-function': ['transition'],
-    '-moz-transition-delay': ['-moz-transition'],
-    '-moz-transition-duration': ['-moz-transition'],
-    '-moz-transition-property': ['-moz-transition'],
-    '-moz-transition-timing-function': ['-moz-transition'],
-    '-o-transition-delay': ['-o-transition'],
-    '-o-transition-duration': ['-o-transition'],
-    '-o-transition-property': ['-o-transition'],
-    '-o-transition-timing-function': ['-o-transition'],
-    '-webkit-transition-delay': ['-webkit-transition'],
-    '-webkit-transition-duration': ['-webkit-transition'],
-    '-webkit-transition-property': ['-webkit-transition'],
-    '-webkit-transition-timing-function': ['-webkit-transition']
-  };
-
-  var compatibility = options.compatibility;
-  var aggressiveMerging = options.aggressiveMerging;
-  var shorthandCompacting = options.shorthandCompacting;
-
-  var IE_BACKSLASH_HACK = '\\9';
-  var processable = processableInfo.processable(compatibility);
+function _optimize(properties, mergeAdjacent, aggressiveMerging) {
+  var overrideMapping = {};
+  var lastName = null;
+  var j;
 
-  var overrides = {};
-  for (var granular in overridable) {
-    for (var i = 0; i < overridable[granular].length; i++) {
-      var coarse = overridable[granular][i];
-      var list = overrides[coarse];
+  function mergeablePosition(position) {
+    if (mergeAdjacent === false || mergeAdjacent === true)
+      return mergeAdjacent;
 
-      if (list)
-        list.push(granular);
-      else
-        overrides[coarse] = [granular];
-    }
+    return mergeAdjacent.indexOf(position) > -1;
   }
 
-  var tokenize = function(properties, selector) {
-    var tokenized = [];
-
-    for (var i = 0, l = properties.length; i < l; i++) {
-      var property = properties[i];
-      var firstColon = property[0].indexOf(':');
-      var name = property[0].substring(0, firstColon);
-      var value = property[0].substring(firstColon + 1);
-      if (value === '') {
-        context.warnings.push('Empty property \'' + name + '\' inside \'' + selector.join(',') + '\' selector. Ignoring.');
-        continue;
-      }
-
-      tokenized.push([
-        name,
-        value,
-        value.indexOf('!important') > -1,
-        property[0].indexOf(IE_BACKSLASH_HACK, firstColon + 1) === property[0].length - IE_BACKSLASH_HACK.length,
-        property.slice(1)
-      ]);
-    }
-
-    return tokenized;
-  };
-
-  var optimize = function(properties, allowAdjacent) {
-    var merged = [];
-    var names = [];
-    var lastName = null;
-    var rescanTrigger = {};
-
-    var removeOverridenBy = function(property, isImportant) {
-      var overrided = overrides[property];
-      for (var i = 0, l = overrided.length; i < l; i++) {
-        for (var j = 0; j < names.length; j++) {
-          if (names[j] != overrided[i] || (merged[j][2] && !isImportant))
-            continue;
-
-          merged.splice(j, 1);
-          names.splice(j, 1);
-          j -= 1;
+  propertyLoop:
+  for (var position = 0, total = properties.length; position < total; position++) {
+    var property = properties[position];
+    var _name = (property.name == '-ms-filter' || property.name == 'filter') ?
+      (lastName == 'background' || lastName == 'background-image' ? lastName : property.name) :
+      property.name;
+    var isImportant = property.important;
+    var isHack = property.hack;
+
+    if (property.unused)
+      continue;
+
+    // comment is necessary - we assume that if two properties are one after another
+    // then it is intentional way of redefining property which may not be widely supported
+    // e.g. a{display:inline-block;display:-moz-inline-box}
+    // however if `mergeablePosition` yields true then the rule does not apply
+    // (e.g merging two adjacent selectors: `a{display:block}a{display:block}`)
+    if ((aggressiveMerging && _name != lastName || mergeablePosition(position)) && _name in overrideMapping) {
+      var toOverridePositions = overrideMapping[_name];
+      var canOverride = compactable[_name] && compactable[_name].canOverride;
+      var anyRemoved = false;
+
+      for (j = toOverridePositions.length - 1; j >= 0; j--) {
+        var toRemove = properties[toOverridePositions[j]];
+        var longhandToShorthand = toRemove.name != _name;
+        var wasImportant = toRemove.important;
+        var wasHack = toRemove.hack;
+
+        if (toRemove.unused)
+          continue;
+
+        if (longhandToShorthand && wasImportant)
+          continue;
+
+        if (!wasImportant && (wasHack && !isHack || !wasHack && isHack))
+          continue;
+
+        if (!wasHack && !isHack && !longhandToShorthand && canOverride && !canOverride(toRemove, property))
+          continue;
+
+        if (wasImportant && !isImportant || wasImportant && isHack) {
+          property.unused = true;
+          continue propertyLoop;
+        } else {
+          anyRemoved = true;
+          toRemove.unused = true;
         }
       }
-    };
-
-    var mergeablePosition = function(position) {
-      if (allowAdjacent === false || allowAdjacent === true)
-        return allowAdjacent;
-
-      return allowAdjacent.indexOf(position) > -1;
-    };
-
-    propertiesLoop:
-    for (var i = 0, l = properties.length; i < l; i++) {
-      var property = properties[i];
-      var name = property[0];
-      var value = property[1];
-      var isImportant = property[2];
-      var isIEHack = property[3];
-      var _name = (name == '-ms-filter' || name == 'filter') ?
-        (lastName == 'background' || lastName == 'background-image' ? lastName : name) :
-        name;
-      var toOverridePosition = 0;
-
-      if (isIEHack && !compatibility.properties.ieSuffixHack)
-        continue;
-
-      // comment is necessary - we assume that if two properties are one after another
-      // then it is intentional way of redefining property which may not be widely supported
-      // e.g. a{display:inline-block;display:-moz-inline-box}
-      // however if `mergeablePosition` yields true then the rule does not apply
-      // (e.g merging two adjacent selectors: `a{display:block}a{display:block}`)
-      if (aggressiveMerging && name !== '' && _name != lastName || mergeablePosition(i)) {
-        while (true) {
-          toOverridePosition = names.indexOf(_name, toOverridePosition);
-          if (toOverridePosition == -1)
-            break;
-
-          var lastToken = merged[toOverridePosition];
-          var wasImportant = lastToken[2];
-          var wasIEHack = lastToken[3];
-
-          if (wasImportant && !isImportant)
-            continue propertiesLoop;
-
-          if (compatibility.properties.ieSuffixHack && !wasIEHack && isIEHack)
-            break;
 
-          var _info = processable[_name];
-          if (!isIEHack && !wasIEHack && _info && _info.canOverride && !_info.canOverride(properties[toOverridePosition][1], value))
-            break;
-
-          merged.splice(toOverridePosition, 1);
-          names.splice(toOverridePosition, 1);
+      if (anyRemoved) {
+        position = -1;
+        overrideMapping = {};
+      }
+    } else {
+      overrideMapping[_name] = overrideMapping[_name] || [];
+      overrideMapping[_name].push(position);
+
+      // TODO: to be removed with
+      // certain shorthand (see values of `shorthands`) should trigger removal of
+      // longhand properties (see keys of `shorthands`)
+      var _shorthands = shorthands[_name];
+      if (_shorthands) {
+        for (j = _shorthands.length - 1; j >= 0; j--) {
+          var shorthand = _shorthands[j];
+          overrideMapping[shorthand] = overrideMapping[shorthand] || [];
+          overrideMapping[shorthand].push(position);
         }
       }
-
-      merged.push(property);
-      names.push(_name);
-
-      // certain properties (see values of `overridable`) should trigger removal of
-      // more granular properties (see keys of `overridable`)
-      if (rescanTrigger[_name])
-        removeOverridenBy(_name, isImportant);
-
-      // add rescan triggers - if certain property appears later in the list a rescan needs
-      // to be triggered, e.g 'border-top' triggers a rescan after 'border-top-width' and
-      // 'border-top-color' as they can be removed
-      for (var j = 0, list = overridable[_name] || [], m = list.length; j < m; j++)
-        rescanTrigger[list[j]] = true;
-
-      lastName = _name;
-    }
-
-    return merged;
-  };
-
-  var rebuild = function(properties) {
-    var rebuilt = [];
-    var eligibleForCompacting = false;
-
-    for (var i = 0, l = properties.length; i < l; i++) {
-      if (!eligibleForCompacting && processableInfo.implementedFor.test(properties[i][0]))
-        eligibleForCompacting = true;
-
-      // FIXME: the check should be gone with #407
-      var property = !properties[i][0] && properties[i][1].indexOf('__ESCAPED_') === 0 ?
-        properties[i][1] :
-        properties[i][0] + ':' + properties[i][1];
-      var metadata = properties[i].pop();
-
-      rebuilt.push([property].concat(metadata));
     }
 
-    return {
-      compactFurther: eligibleForCompacting,
-      list: rebuilt
-    };
-  };
-
-  var compact = function (input) {
-    var Token = processableInfo.Token;
-
-    var tokens = Token.tokenize(input);
-
-    tokens = overrideCompactor.compactOverrides(tokens, processable, Token, compatibility);
-    tokens = shorthandCompactor.compactShorthands(tokens, false, processable, Token);
-    tokens = shorthandCompactor.compactShorthands(tokens, true, processable, Token);
-
-    return Token.detokenize(tokens);
-  };
+    lastName = _name;
+  }
+}
+
+function optimize(selector, properties, mergeAdjacent, options) {
+  var _properties = wrapForOptimizing(properties);
+  populateComponents(_properties);
+  _optimize(_properties, mergeAdjacent, options.aggressiveMerging);
+
+  // TODO: we removed option to manually turn compacting off
+  // may result in performance regression
+  if (options.shorthandCompacting && !options.sourceMap) {
+    compactOverrides(_properties, options.compatibility);
+    // compactShorthands(_properties, false, options.compatibility);
+    // compactShorthands(_properties, true, options.compatibility);
+  }
 
-  return {
-    process: function(selector, properties, allowAdjacent, compactProperties) {
-      var tokenized = tokenize(properties, selector);
-      var optimized = optimize(tokenized, allowAdjacent);
-      var rebuilt = rebuild(optimized);
+  restoreShorthands(_properties);
+  removeUnused(_properties);
+}
 
-      return shorthandCompacting && compactProperties && rebuilt.compactFurther ?
-        compact(rebuilt.list) :
-        rebuilt.list;
-    }
-  };
-};
+module.exports = optimize;
index 7b2981d..1e8143e 100644 (file)
-
-// Compacts the given tokens according to their ability to override each other.
-
-var validator = require('./validator');
-
-module.exports = (function () {
-  // Default override function: only allow overrides when the two values are the same
-  var sameValue = function (val1, val2) {
-    return val1 === val2;
+var canOverride = require('./can-override');
+var compactable = require('./compactable');
+var shallowClone = require('./shallow-clone');
+
+// Used when searching for a component that matches property
+function nameMatchFilter(to) {
+  return function (property) {
+    return to.name === property.name;
   };
+}
+
+function wouldBreakCompatibility(property) {
+  for (var i = 0; i < property.components.length; i++) {
+    var component = property.components[i];
+    var descriptor = compactable[component.name];
+    var canOverride = descriptor && descriptor.canOverride || canOverride.sameValue;
+
+    var _component = shallowClone(component);
+    _component.value = [[descriptor.defaultValue]];
+
+    if (!canOverride(_component, component))
+      return true;
+  }
+
+  return false;
+}
+
+function isComponentOf(shorthand, longhand) {
+  return compactable[shorthand.name].components.indexOf(longhand.name) > -1;
+}
+
+function overrideSimple(property, by) {
+  by.unused = true;
+  property.value = by.value;
+}
+
+function overrideMultiplex(property, by) {
+  by.unused = true;
+
+  for (var i = 0; i < property.value.length; i++) {
+    property.value[i] = by.value;
+  }
+}
+
+function override(property, by) {
+  if (property.multiplex)
+    overrideMultiplex(property, by);
+  else
+    overrideSimple(property, by);
+}
+
+function overrideShorthand(property, by) {
+  by.unused = true;
+
+  for (var i = 0, l = property.components.length; i < l; i++) {
+    override(property.components[i], by.components[i]);
+  }
+}
+
+function hasInherits(property) {
+  for (var i = property.value.length - 1; i >= 0; i--) {
+    if (property.value[i] == 'inherit')
+      return true;
+  }
+
+  return false;
+}
+
+function compactOverrides(properties, compatibility) {
+  var mayOverride, right, left, component;
+  var i, j, k;
+
+  propertyLoop:
+  for (i = properties.length - 1; i >= 0; i--) {
+    right = properties[i];
+    mayOverride = (compactable[right.name] && compactable[right.name].canOverride) || canOverride.sameValue;
+
+    for (j = i - 1; j >= 0; j--) {
+      left = properties[j];
+
+      if (left.unused || right.unused)
+        continue;
+
+      if (!left.shorthand && right.shorthand && isComponentOf(right, left)) {
+        // maybe `left` can be overridden by `right` which is a shorthand?
+        // TODO: this is actually more complex, as in some cases it's better to incorporate the value, e.g.
+        // background:url(...); background-repeat:no-repeat,no-repeat;
+        // background:url(...) no-repeat,no-repeat;
+        if (!right.multiplex && left.multiplex)
+          continue;
 
-  var compactOverrides = function (tokens, processable, Token, compatibility) {
-    var result, can, token, t, i, ii, iiii, 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;
-    };
-
-    function willResultInShorterValue (shorthand, token) {
-      var shorthandCopy = shorthand.clone();
-      shorthandCopy.isDirty = true;
-      shorthandCopy.isShorthand = true;
-      shorthandCopy.components = [];
-
-      shorthand.components.forEach(function (component) {
-        var componentCopy = component.clone();
-        if (component.prop == token.prop)
-          componentCopy.value = token.value;
-
-        shorthandCopy.components.push(componentCopy);
-      });
-
-      return Token.getDetokenizedLength([shorthand, token]) >= Token.getDetokenizedLength([shorthandCopy]);
-    }
-
-    // Go from the end and always take what the current token can't override as the new result set
-    // NOTE: can't cache result.length here because it will change with every iteration
-    for (result = tokens, i = 0; (ii = result.length - 1 - i) >= 0; i++) {
-      token = result[ii];
-      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;
-      var oldResultLength = oldResult.length;
-
-      for (var iii = 0; iii < oldResultLength; 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);
+        if (!right.important && left.important)
           continue;
+
+        component = right.components.filter(nameMatchFilter(left))[0];
+        mayOverride = (compactable[left.name] && compactable[left.name].canOverride) || canOverride.sameValue;
+        if (mayOverride(left, component)) {
+          left.unused = true;
         }
+      } else if (left.shorthand && !right.shorthand && isComponentOf(left, right)) {
+        // maybe `right` can be pulled into `left` which is a shorthand?
+        // TODO - see above
+        if (right.multiplex && !left.multiplex)
+          continue;
 
-        // Only an important token can even try to override tokens that come after it
-        if (iii > ii && !token.isImportant) {
-          result.push(t);
+        if (right.important && !left.important)
           continue;
+
+        component = left.components.filter(nameMatchFilter(right))[0];
+        if (mayOverride(component, right)) {
+          var disabledBackgroundSizeMerging = !compatibility.properties.backgroundSizeMerging && component.name.indexOf('background-size') > -1;
+          var nonMergeableValue = compactable[right.name].nonMergeableValue === right.value[0][0];
+
+          if (disabledBackgroundSizeMerging || nonMergeableValue)
+            continue;
+
+          if (!compatibility.properties.merging && wouldBreakCompatibility(left))
+            continue;
+
+          if (component.value[0][0] != right.value[0][0] && (hasInherits(left) || hasInherits(right)))
+            continue;
+
+          override(component, right);
+          left.dirty = true;
         }
+      } else if (left.shorthand && right.shorthand && left.name == right.name) {
+        // merge if all components can be merged
 
-        // If an important component tries to override an important shorthand and it is not yet merged
-        // just make sure it is not lost
-        if (iii > ii && t.isImportant && token.isImportant && t.prop != token.prop && t.isComponentOf(token)) {
-          result.push(t);
-          continue;
+        if (!right.important && left.important) {
+          right.unused = true;
+          continue propertyLoop;
         }
 
-        // A nonimportant token can never override an important one
-        if (t.isImportant && !token.isImportant) {
-          result.push(t);
+        if (right.important && !left.important) {
+          left.unused = true;
           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
-            var disabledForToken = !compatibility.properties.backgroundSizeMerging && token.prop.indexOf('background-size') > -1 ||
-              processable[token.prop].nonMergeableValue && processable[token.prop].nonMergeableValue == token.value;
-
-            if (disabledForToken) {
-              result.push(t);
-              continue;
-            }
-
-            if (!compatibility.properties.merging) {
-              // in compatibility mode check if shorthand in not less understandable than merged-in value
-              var wouldBreakCompatibility = false;
-              for (iiii = 0; iiii < t.components.length; iiii++) {
-                var o = processable[t.components[iiii].prop];
-                can = (o && o.canOverride) || sameValue;
-
-                if (!can(o.defaultValue, t.components[iiii].value)) {
-                  wouldBreakCompatibility = true;
-                  break;
-                }
-              }
-
-              if (wouldBreakCompatibility) {
-                result.push(t);
-                continue;
-              }
-            }
-
-            if ((!token.isImportant || token.isImportant && matchingComponent.isImportant) && willResultInShorterValue(t, token)) {
-              // 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 (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;
-            }
-            if (t.components[iiii].isImportant && token.components[iiii].isImportant && (validator.isValidFunction(t.components[iiii].value) ^ validator.isValidFunction(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);
-        } else if (t.isImportant && token.isImportant && (validator.isValidFunction(t.value) ^ validator.isValidFunction(token.value))) {
-          result.push(t);
+        for (k = left.components.length - 1; k >= 0; k--) {
+          var leftComponent = left.components[k];
+          var rightComponent = right.components[k];
+
+          mayOverride = compactable[leftComponent.name].canOverride || canOverride.sameValue;
+          if (!mayOverride(leftComponent, rightComponent) || !canOverride.twoOptionalFunctions(leftComponent, rightComponent))
+            continue propertyLoop;
         }
-      }
-      if (removeSelf) {
-        i--;
+
+        overrideShorthand(left, right);
+        left.dirty = true;
       }
     }
+  }
+}
 
-    return result;
-  };
-
-  return {
-    compactOverrides: compactOverrides
-  };
-
-})();
+module.exports = compactOverrides;
diff --git a/lib/properties/populate-components.js b/lib/properties/populate-components.js
new file mode 100644 (file)
index 0000000..8e4d203
--- /dev/null
@@ -0,0 +1,21 @@
+var compactable = require('./compactable');
+
+function populateComponents(properties) {
+  for (var i = properties.length - 1; i >= 0; i--) {
+    var property = properties[i];
+    var descriptor = compactable[property.name];
+
+    if (descriptor && descriptor.shorthand) {
+      property.shorthand = true;
+      property.dirty = true;
+      property.components = descriptor.breakUp(property, compactable);
+
+      if (property.components.length > 0)
+        property.multiplex = Array.isArray(property.components[0].value[0][0]);
+      else
+        property.unused = true;
+    }
+  }
+}
+
+module.exports = populateComponents;
diff --git a/lib/properties/processable.js b/lib/properties/processable.js
deleted file mode 100644 (file)
index 60dc250..0000000
+++ /dev/null
@@ -1,920 +0,0 @@
-
-// Contains the interpretation of CSS properties, as used by the property optimizer
-
-module.exports = (function () {
-
-  var tokenModule = require('./token');
-  var validator = require('./validator');
-  var Splitter = require('../utils/splitter');
-
-  // 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.isValidAndCompatibleUnitWithoutFunction(val1) && !validator.isValidAndCompatibleUnitWithoutFunction(val2))
-        return false;
-
-      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.isValidHslaColor(val2))
-        return true;
-      if (validator.isValidRgbaColor(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);
-    },
-    border: function(val1, val2) {
-      var brokenUp1 = breakUp.border(Token.tokenizeOne([val1]));
-      var brokenUp2 = breakUp.border(Token.tokenizeOne([val2]));
-
-      return canOverride.color(brokenUp1[2].value, brokenUp2[2].value);
-    }
-  };
-  canOverride = Object.freeze(canOverride);
-
-  // Functions for breaking up shorthands to components
-  var breakUp = {};
-  breakUp.takeCareOfFourValues = function (splitfunc) {
-    return function (token) {
-      var descriptor = processable[token.prop];
-      var result = [];
-      var splitval = splitfunc(token.value);
-
-      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;
-    };
-  };
-  // Use this when you simply want to break up four values along spaces
-  breakUp.fourBySpaces = breakUp.takeCareOfFourValues(function (val) {
-    return new Splitter(' ').split(val).filter(function (v) { return v; });
-  });
-  // Breaks up a background property value
-  breakUp.commaSeparatedMulitpleValues = function (splitfunc) {
-    return function (token) {
-      if (token.value.indexOf(',') === -1)
-        return splitfunc(token);
-
-      var values = new Splitter(',').split(token.value);
-      var components = [];
-
-      // TODO: we should be rather clonging elements than reusing them!
-      for (var i = 0, l = values.length; i < l; i++) {
-        token.value = values[i];
-        components.push(splitfunc(token));
-      }
-
-      token.value = values.join(',');
-
-      for (var j = 0, m = components[0].length; j < m; j++) {
-        for (var k = 0, n = components.length, newValues = []; k < n; k++) {
-          newValues.push(components[k][j].value);
-        }
-
-        components[0][j].value = newValues.join(',');
-      }
-
-      return components[0];
-    };
-  };
-  breakUp.background = function (token) {
-    // Default values
-    var result = Token.makeDefaults(['background-image', 'background-position', 'background-size', 'background-repeat', 'background-attachment', 'background-origin', 'background-clip', 'background-color'], token.isImportant);
-    var image = result[0];
-    var position = result[1];
-    var size = result[2];
-    var repeat = result[3];
-    var attachment = result[4];
-    var origin = result[5];
-    var clip = result[6];
-    var color = result[7];
-    var positionSet = false;
-    var clipSet = false;
-    var originSet = false;
-    var repeatSet = false;
-
-    // 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 = size.value = attachment.value = origin.value = clip.value = 'inherit';
-      return result;
-    }
-
-    // Break the background up into parts
-    var parts = new Splitter(' ').split(token.value);
-    if (parts.length === 0)
-      return result;
-
-    // Iterate over all parts and try to fit them into positions
-    for (var i = parts.length - 1; i >= 0; i--) {
-      var currentPart = parts[i];
-
-      if (validator.isValidBackgroundAttachment(currentPart)) {
-        attachment.value = currentPart;
-      } else if (validator.isValidBackgroundBox(currentPart)) {
-        if (clipSet) {
-          origin.value = currentPart;
-          originSet = true;
-        } else {
-          clip.value = currentPart;
-          clipSet = true;
-        }
-      } else if (validator.isValidBackgroundRepeat(currentPart)) {
-        if (repeatSet) {
-          repeat.value = currentPart + ' ' + repeat.value;
-        } else {
-          repeat.value = currentPart;
-          repeatSet = true;
-        }
-      } else if (validator.isValidBackgroundPositionPart(currentPart) || validator.isValidBackgroundSizePart(currentPart)) {
-        if (i > 0) {
-          var previousPart = parts[i - 1];
-
-          if (previousPart.indexOf('/') > 0) {
-            var twoParts = new Splitter('/').split(previousPart);
-            size.value = twoParts.pop() + ' ' + currentPart;
-            parts[i - 1] = twoParts.pop();
-          } else if (i > 1 && parts[i - 2] == '/') {
-            size.value = previousPart + ' ' + currentPart;
-            i -= 2;
-          } else if (parts[i - 1] == '/') {
-            size.value = currentPart;
-          } else {
-            position.value = currentPart + (positionSet ? ' ' + position.value : '');
-            positionSet = true;
-          }
-        } else {
-          position.value = currentPart + (positionSet ? ' ' + position.value : '');
-          positionSet = true;
-        }
-      } else if (validator.isValidBackgroundPositionAndSize(currentPart)) {
-        var sizeValue = new Splitter('/').split(currentPart);
-        size.value = sizeValue.pop();
-        position.value = sizeValue.pop();
-      } else if ((color.value == processable[color.prop].defaultValue || color.value == 'none') && validator.isValidColor(currentPart)) {
-        color.value = currentPart;
-      } else if (validator.isValidUrl(currentPart) || validator.isValidFunction(currentPart)) {
-        image.value = currentPart;
-      }
-    }
-
-    if (clipSet && !originSet)
-      origin.value = clip.value;
-
-    return result;
-  };
-  // Breaks up a list-style property value
-  breakUp.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 = new Splitter(' ').split(token.value);
-    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;
-  };
-
-  breakUp._widthStyleColor = function(token, prefix, order) {
-    // Default values
-    var components = order.map(function(prop) {
-      return prefix + '-' + prop;
-    });
-    var result = Token.makeDefaults(components, token.isImportant);
-    var color = result[order.indexOf('color')];
-    var style = result[order.indexOf('style')];
-    var width = result[order.indexOf('width')];
-
-    // 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 = new Splitter(' ').split(token.value), w;
-
-    if (parts.length === 0) {
-      return result;
-    }
-
-    if (parts.length >= 1) {
-      // Try to find -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 -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 -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.outline = function(token) {
-    return breakUp._widthStyleColor(token, 'outline', ['color', 'style', 'width']);
-  };
-
-  breakUp.border = function(token) {
-    return breakUp._widthStyleColor(token, 'border', ['width', 'style', 'color']);
-  };
-
-  breakUp.borderRadius = function(token) {
-    var parts = token.value.split('/');
-    if (parts.length == 1)
-      return breakUp.fourBySpaces(token);
-
-    var horizontalPart = token.clone();
-    var verticalPart = token.clone();
-
-    horizontalPart.value = parts[0];
-    verticalPart.value = parts[1];
-
-    var horizontalBreakUp = breakUp.fourBySpaces(horizontalPart);
-    var verticalBreakUp = breakUp.fourBySpaces(verticalPart);
-
-    for (var i = 0; i < 4; i++) {
-      horizontalBreakUp[i].value = [horizontalBreakUp[i].value, verticalBreakUp[i].value];
-    }
-
-    return horizontalBreakUp;
-  };
-
-  // 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, meta) {
-      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];
-        var definition = processable[token.prop] && processable[token.prop];
-
-        // 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;
-          }
-        }
-
-        // merge with previous if possible
-        if (definition.mergeWithPrevious && token.value === tokens[i - 1].value)
-          continue;
-
-        // omit irrelevant value
-        if (token.isIrrelevant)
-          continue;
-
-        // omit default value unless mergable with previous and it wasn't default
-        if (definition.defaultValue === token.value)
-          if (!definition.mergeWithPrevious || tokens[i - 1].value === processable[tokens[i - 1].prop].defaultValue)
-            continue;
-
-        if (meta && meta.partsCount && meta.position < meta.partsCount - 1 && definition.multiValueLastOnly)
-          continue;
-
-        var requiresPreceeding = definition.shorthandFollows;
-        if (requiresPreceeding && (tokens[i - 1].value == processable[requiresPreceeding].defaultValue)) {
-          result.value += ' ' + tokens[i - 1].value;
-        }
-
-        result.value += (definition.prefixShorthandValueWith || ' ') + token.value;
-      }
-
-      result.value = result.value.trim();
-      if (!result.value) {
-        result.value = valueIfAllDefault;
-      }
-
-      return result;
-    },
-    commaSeparatedMulitpleValues: function (assembleFunction) {
-      return function(prop, tokens, isImportant) {
-        var tokenSplitLengths = tokens.map(function (token) {
-          return new Splitter(',').split(token.value).length;
-        });
-        var partsCount = Math.max.apply(Math, tokenSplitLengths);
-
-        if (partsCount == 1)
-          return assembleFunction(prop, tokens, isImportant);
-
-        var merged = [];
-
-        for (var i = 0; i < partsCount; i++) {
-          merged.push([]);
-
-          for (var j = 0; j < tokens.length; j++) {
-            var split = new Splitter(',').split(tokens[j].value);
-            merged[i].push(split[i] || split[0]);
-          }
-        }
-
-        var mergedValues = [];
-        var firstProcessed;
-        for (i = 0; i < partsCount; i++) {
-          var newTokens = [];
-          for (var k = 0, n = merged[i].length; k < n; k++) {
-            var newToken = tokens[k].clone();
-            newToken.value = merged[i][k];
-            newTokens.push(newToken);
-          }
-
-          var meta = {
-            partsCount: partsCount,
-            position: i
-          };
-          var processed = assembleFunction(prop, newTokens, isImportant, meta);
-          mergedValues.push(processed.value);
-
-          if (!firstProcessed)
-            firstProcessed = processed;
-        }
-
-        firstProcessed.value = mergedValues.join(',');
-        return firstProcessed;
-      };
-    },
-    // Handles the cases when some or all the fine-grained properties are set to inherit
-    takeCareOfInherit: function (innerFunc) {
-      return function (prop, tokens, isImportant, meta) {
-        // 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]);
-          }
-        }
-
-        if (nonInheritingTokens.length === 0) {
-          // When all the tokens are 'inherit'
-          return new Token(prop, 'inherit', isImportant);
-        } else if (inheritingTokens.length > 0) {
-          // When some (but not all) of the tokens are 'inherit'
-
-          // 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, meta)].concat(inheritingTokens);
-
-          // Return whichever is shorter
-          var dl1 = Token.getDetokenizedLength(result1);
-          var dl2 = Token.getDetokenizedLength(result2);
-
-          return dl1 < dl2 ? result1 : result2;
-        } else {
-          // When none of tokens are 'inherit'
-          return innerFunc(prop, tokens, isImportant, meta);
-        }
-      };
-    },
-    borderRadius: function (prop, tokens, isImportant) {
-      var verticalTokens = [];
-      var newTokens = [];
-
-      for (var i = 0, l = tokens.length; i < l; i++) {
-        var token = tokens[i];
-        var newToken = token.clone();
-        newTokens.push(newToken);
-        if (!Array.isArray(token.value))
-          continue;
-
-        if (token.value.length > 1) {
-          verticalTokens.push({
-            prop: token.prop,
-            value: token.value[1],
-            isImportant: token.isImportant
-          });
-        }
-
-        newToken.value = token.value[0];
-      }
-
-      var result = putTogether.takeCareOfInherit(putTogether.fourUnits)(prop, newTokens, isImportant);
-      if (verticalTokens.length > 0) {
-        var verticalResult = putTogether.takeCareOfInherit(putTogether.fourUnits)(prop, verticalTokens, isImportant);
-        if (result.value != verticalResult.value)
-          result.value += '/' + verticalResult.value;
-      }
-
-      return result;
-    }
-  };
-
-  // 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 = {
-    'color': {
-      canOverride: canOverride.color,
-      defaultValue: 'transparent',
-      shortestValue: 'red'
-    },
-    // background ------------------------------------------------------------------------------
-    'background': {
-      components: [
-        'background-image',
-        'background-position',
-        'background-size',
-        'background-repeat',
-        'background-attachment',
-        'background-origin',
-        'background-clip',
-        'background-color'
-      ],
-      breakUp: breakUp.commaSeparatedMulitpleValues(breakUp.background),
-      putTogether: putTogether.commaSeparatedMulitpleValues(
-        putTogether.takeCareOfInherit(putTogether.bySpacesOmitDefaults)
-      ),
-      defaultValue: '0 0',
-      shortestValue: '0'
-    },
-    'background-clip': {
-      canOverride: canOverride.always,
-      defaultValue: 'border-box',
-      shortestValue: 'border-box',
-      shorthandFollows: 'background-origin',
-      mergeWithPrevious: true
-    },
-    'background-color': {
-      canOverride: canOverride.color,
-      defaultValue: 'transparent',
-      multiValueLastOnly: true,
-      nonMergeableValue: 'none',
-      shortestValue: 'red'
-    },
-    'background-image': {
-      canOverride: canOverride.backgroundImage,
-      defaultValue: 'none'
-    },
-    'background-origin': {
-      canOverride: canOverride.always,
-      defaultValue: 'padding-box',
-      shortestValue: 'border-box'
-    },
-    'background-repeat': {
-      canOverride: canOverride.always,
-      defaultValue: 'repeat'
-    },
-    'background-position': {
-      canOverride: canOverride.always,
-      defaultValue: '0 0',
-      shortestValue: '0'
-    },
-    'background-size': {
-      canOverride: canOverride.always,
-      defaultValue: 'auto',
-      shortestValue: '0 0',
-      prefixShorthandValueWith: '/',
-      shorthandFollows: 'background-position'
-    },
-    'background-attachment': {
-      canOverride: canOverride.always,
-      defaultValue: 'scroll'
-    },
-    'border': {
-      breakUp: breakUp.border,
-      canOverride: canOverride.border,
-      components: [
-        'border-width',
-        'border-style',
-        'border-color'
-      ],
-      defaultValue: 'none',
-      putTogether: putTogether.takeCareOfInherit(putTogether.bySpacesOmitDefaults)
-    },
-    'border-color': {
-      canOverride: canOverride.color,
-      defaultValue: 'none'
-    },
-    'border-style': {
-      canOverride: canOverride.always,
-      defaultValue: 'none'
-    },
-    'border-width': {
-      canOverride: canOverride.unit,
-      defaultValue: 'medium',
-      shortestValue: '0'
-    },
-    // list-style ------------------------------------------------------------------------------
-    '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 ------------------------------------------------------------------------------
-    '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'
-    },
-    // transform
-    '-moz-transform': {
-      canOverride: canOverride.sameFunctionOrValue
-    },
-    '-ms-transform': {
-      canOverride: canOverride.sameFunctionOrValue
-    },
-    '-webkit-transform': {
-      canOverride: canOverride.sameFunctionOrValue
-    },
-    'transform': {
-      canOverride: canOverride.sameFunctionOrValue
-    }
-  };
-
-  var addFourValueShorthand = function (prop, components, options) {
-    options = options || {};
-    processable[prop] = {
-      components: components,
-      breakUp: options.breakUp || breakUp.fourBySpaces,
-      putTogether: options.putTogether || putTogether.takeCareOfInherit(putTogether.fourUnits),
-      defaultValue: options.defaultValue || '0',
-      shortestValue: options.shortestValue
-    };
-    for (var i = 0; i < components.length; i++) {
-      processable[components[i]] = {
-        breakUp: options.breakUp || breakUp.fourBySpaces,
-        canOverride: options.canOverride || canOverride.unit,
-        defaultValue: options.defaultValue || '0',
-        shortestValue: options.shortestValue
-      };
-    }
-  };
-
-  ['', '-moz-', '-o-', '-webkit-'].forEach(function (prefix) {
-    addFourValueShorthand(prefix + 'border-radius', [
-      prefix + 'border-top-left-radius',
-      prefix + 'border-top-right-radius',
-      prefix + 'border-bottom-right-radius',
-      prefix + 'border-bottom-left-radius'
-    ], {
-      breakUp: breakUp.borderRadius,
-      putTogether: putTogether.borderRadius
-    });
-  });
-
-  addFourValueShorthand('border-color', [
-    'border-top-color',
-    'border-right-color',
-    'border-bottom-color',
-    'border-left-color'
-  ], {
-    breakUp: breakUp.fourBySpaces,
-    canOverride: canOverride.color,
-    defaultValue: 'currentColor',
-    shortestValue: 'red'
-  });
-
-  addFourValueShorthand('border-style', [
-    'border-top-style',
-    'border-right-style',
-    'border-bottom-style',
-    'border-left-style'
-  ], {
-    breakUp: breakUp.fourBySpaces,
-    canOverride: canOverride.always,
-    defaultValue: 'none'
-  });
-
-  addFourValueShorthand('border-width', [
-    'border-top-width',
-    'border-right-width',
-    'border-bottom-width',
-    'border-left-width'
-  ], {
-    defaultValue: 'medium',
-    shortestValue: '0'
-  });
-
-  addFourValueShorthand('padding', [
-    'padding-top',
-    'padding-right',
-    'padding-bottom',
-    'padding-left'
-  ]);
-
-  addFourValueShorthand('margin', [
-    'margin-top',
-    'margin-right',
-    'margin-bottom',
-    'margin-left'
-  ]);
-
-  // Set some stuff iteratively
-  for (var proc in processable) {
-    if (!processable.hasOwnProperty(proc))
-      continue;
-
-    var currDesc = processable[proc];
-
-    if (!(currDesc.components instanceof Array) || currDesc.components.length === 0)
-      continue;
-
-    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 = tokenModule.createTokenPrototype(processable);
-
-  return {
-    implementedFor: /background|border|color|list|margin|outline|padding|transform/,
-    processable: function (compatibility) {
-      // FIXME: we need a proper OO way
-      validator.setCompatibility(compatibility);
-
-      return processable;
-    },
-    Token: Token
-  };
-})();
diff --git a/lib/properties/remove-unused.js b/lib/properties/remove-unused.js
new file mode 100644 (file)
index 0000000..9d8cf64
--- /dev/null
@@ -0,0 +1,8 @@
+function removeUnused(properties) {
+  for (var i = properties.length - 1; i >= 0; i--) {
+    if (properties[i].unused)
+      properties[i].all.splice(i, 1);
+  }
+}
+
+module.exports = removeUnused;
diff --git a/lib/properties/restore-shorthands.js b/lib/properties/restore-shorthands.js
new file mode 100644 (file)
index 0000000..579038b
--- /dev/null
@@ -0,0 +1,18 @@
+var compactable = require('./compactable');
+
+function restoreShorthands(properties) {
+  for (var i = properties.length - 1; i >= 0; i--) {
+    var property = properties[i];
+    var descriptor = compactable[property.name];
+
+    if (descriptor && descriptor.shorthand && property.dirty && !property.unused) {
+      var restored = descriptor.restore(property, compactable);
+      var current = property.all[property.position];
+      current.splice(1, current.length - 1);
+
+      Array.prototype.push.apply(current, restored);
+    }
+  }
+}
+
+module.exports = restoreShorthands;
diff --git a/lib/properties/restore.js b/lib/properties/restore.js
new file mode 100644 (file)
index 0000000..52e2e33
--- /dev/null
@@ -0,0 +1,186 @@
+var shallowClone = require('./shallow-clone');
+
+function background(property, compactable) {
+  var components = property.components;
+  var restored = [];
+  var needsOne, needsBoth;
+
+  function restoreValue(component) {
+    Array.prototype.unshift.apply(restored, component.value);
+  }
+
+  function isDefaultValue(component) {
+    var descriptor = compactable[component.name];
+    if (descriptor.doubleValues) {
+      if (descriptor.defaultValue.length == 1)
+        return component.value[0][0] == descriptor.defaultValue[0] && (component.value[1] ? component.value[1][0] == descriptor.defaultValue[0] : true);
+      else
+        return component.value[0][0] == descriptor.defaultValue[0] && (component.value[1] ? component.value[1][0] : component.value[0][0]) == descriptor.defaultValue[1];
+    } else {
+      return component.value[0][0] == descriptor.defaultValue;
+    }
+  }
+
+  for (var i = components.length - 1; i >= 0; i--) {
+    var component = components[i];
+    var isDefault = isDefaultValue(component);
+
+    if (component.name == 'background-clip') {
+      var originComponent = components[i - 1];
+      var isOriginDefault = isDefaultValue(originComponent);
+
+      needsOne = component.value[0][0] == originComponent.value[0][0];
+
+      needsBoth = !needsOne && (
+        (isOriginDefault && !isDefault) ||
+        (!isOriginDefault && !isDefault) ||
+        (!isOriginDefault && isDefault && component.value[0][0] != originComponent.value[0][0]));
+
+      if (needsOne) {
+        restoreValue(originComponent);
+      } else if (needsBoth) {
+        restoreValue(component);
+        restoreValue(originComponent);
+      }
+
+      i--;
+    } else if (component.name == 'background-size') {
+      var positionComponent = components[i - 1];
+      var isPositionDefault = isDefaultValue(positionComponent);
+
+      needsOne = !isPositionDefault && isDefault;
+
+      needsBoth = !needsOne &&
+        (isPositionDefault && !isDefault || !isPositionDefault && !isDefault);
+
+      if (needsOne) {
+        restoreValue(positionComponent);
+      } else if (needsBoth) {
+        restoreValue(component);
+        restored.unshift(['/']);
+        restoreValue(positionComponent);
+      } else if (positionComponent.value.length == 1) {
+        restoreValue(positionComponent);
+      }
+
+      i--;
+    } else {
+      if (!isDefault)
+        restoreValue(component);
+    }
+  }
+
+  if (restored.length === 0 && property.value.length == 1 && property.value[0][0] == '0')
+    restored.push(property.value[0]);
+
+  if (restored.length === 0)
+    restored.push([compactable[property.name].defaultValue]);
+
+  return restored;
+}
+
+function borderRadius(property, compactable) {
+  if (property.multiplex) {
+    var horizontal = shallowClone(property);
+    var vertical = shallowClone(property);
+
+    for (var i = 0; i < 4; i++) {
+      var component = property.components[i];
+
+      var horizontalComponent = shallowClone(property);
+      horizontalComponent.value = component.value[0];
+      horizontal.components.push(horizontalComponent);
+
+      var verticalComponent = shallowClone(property);
+      verticalComponent.value = component.value[1];
+      vertical.components.push(verticalComponent);
+    }
+
+    var horizontalValues = fourValues(horizontal, compactable);
+    var verticalValues = fourValues(vertical, compactable);
+
+    if (horizontalValues.length == verticalValues.length &&
+        horizontalValues[0][0] == verticalValues[0][0] &&
+        (horizontalValues.length > 1 ? horizontalValues[1][0] == verticalValues[1][0] : true) &&
+        (horizontalValues.length > 2 ? horizontalValues[2][0] == verticalValues[2][0] : true) &&
+        (horizontalValues.length > 3 ? horizontalValues[3][0] == verticalValues[3][0] : true)) {
+      return horizontalValues;
+    } else {
+      return horizontalValues.concat([['/']]).concat(verticalValues);
+    }
+  } else {
+    return fourValues(property, compactable);
+  }
+}
+
+function fourValues(property) {
+  var components = property.components;
+  var value1 = components[0].value[0];
+  var value2 = components[1].value[0];
+  var value3 = components[2].value[0];
+  var value4 = components[3].value[0];
+
+  if (value1[0] == value2[0] && value1[0] == value3[0] && value1[0] == value4[0]) {
+    return [value1];
+  } else if (value1[0] == value3[0] && value2[0] == value4[0]) {
+    return [value1, value2];
+  } else if (value2[0] == value4[0]) {
+    return [value1, value2, value3];
+  } else {
+    return [value1, value2, value3, value4];
+  }
+}
+
+function multipleValues(restoreWith) {
+  return function (property, compactable) {
+    if (!property.multiplex)
+      return restoreWith(property, compactable);
+
+    var repeatCounts = property.components[0].value.length;
+    var restored = [];
+
+    for (var i = 0; i < repeatCounts; i++) {
+      var _property = shallowClone(property);
+
+      for (var j = 0, m = property.components.length; j < m; j++) {
+        var _component = shallowClone(property.components[j]);
+        _component.value = property.components[j].value[i];
+        _property.components.push(_component);
+      }
+
+      var _restored = restoreWith(_property, compactable);
+      Array.prototype.push.apply(restored, _restored);
+
+      if (i < repeatCounts - 1)
+        restored.push([',']);
+    }
+
+    return restored;
+  };
+}
+
+function withoutDefaults(property, compactable) {
+  var components = property.components;
+  var restored = [];
+
+  for (var i = components.length - 1; i >= 0; i--) {
+    var component = components[i];
+    var descriptor = compactable[component.name];
+
+    if (component.value[0][0] != descriptor.defaultValue)
+      restored.unshift(component.value[0]);
+  }
+
+  if (restored.length === 0)
+    restored.push([compactable[property.name].defaultValue]);
+
+  return restored;
+}
+
+module.exports = {
+  background: background,
+  borderRadius: borderRadius,
+  fourValues: fourValues,
+  multipleValues: multipleValues,
+  withoutDefaults: withoutDefaults
+};
diff --git a/lib/properties/shallow-clone.js b/lib/properties/shallow-clone.js
new file mode 100644 (file)
index 0000000..b810e90
--- /dev/null
@@ -0,0 +1,9 @@
+var wrapSingle = require('./wrap-for-optimizing').single;
+
+function shallowClone(property) {
+  var cloned = wrapSingle([[property.name, property.important, property.hack]]);
+  cloned.unused = false;
+  return cloned;
+}
+
+module.exports = shallowClone;
index 5fffef2..0c94f31 100644 (file)
 
-// Compacts the tokens by transforming properties into their shorthand notations when possible
-
-module.exports = (function () {
-  var isHackValue = function (t) { return t.value === '__hack'; };
-
-  var compactShorthands = function(tokens, isImportant, processable, Token) {
-    // Contains the components found so far, grouped by shorthand name
-    var componentsSoFar = { };
-
-    // Initializes a prop in componentsSoFar
-    var initSoFar = function (shprop, last, clearAll) {
-      var found = {};
-      var shorthandPosition;
-
-      if (!clearAll && componentsSoFar[shprop]) {
-        for (var i = 0; i < processable[shprop].components.length; i++) {
-          var prop = processable[shprop].components[i];
-          found[prop] = [];
-
-          if (!(componentsSoFar[shprop].found[prop]))
-            continue;
-
-          for (var ii = 0; ii < componentsSoFar[shprop].found[prop].length; ii++) {
-            var comp = componentsSoFar[shprop].found[prop][ii];
-
-            if (comp.isMarkedForDeletion)
-              continue;
-
-            found[prop].push(comp);
-
-            if (comp.position && (!shorthandPosition || comp.position < shorthandPosition))
-              shorthandPosition = comp.position;
-          }
-        }
-      }
-      componentsSoFar[shprop] = {
-        lastShorthand: last,
-        found: found,
-        shorthandPosition: shorthandPosition
-      };
-    };
-
-    // Adds a component to componentsSoFar
-    var addComponentSoFar = function (token, index) {
-      var shprop = processable[token.prop].componentOf;
-      if (!componentsSoFar[shprop])
-        initSoFar(shprop);
-      if (!componentsSoFar[shprop].found[token.prop])
-        componentsSoFar[shprop].found[token.prop] = [];
-
-      // Add the newfound component to componentsSoFar
-      componentsSoFar[shprop].found[token.prop].push(token);
-
-      if (!componentsSoFar[shprop].shorthandPosition && index) {
-        // If the haven't decided on where the shorthand should go, put it in the place of this component
-        componentsSoFar[shprop].shorthandPosition = index;
-      }
-    };
-
-    // Tries to compact a prop in componentsSoFar
-    var compactSoFar = function (prop) {
-      var i;
-      var componentsCount = processable[prop].components.length;
-
-      // Check basics
-      if (!componentsSoFar[prop] || !componentsSoFar[prop].found)
-        return false;
-
-      // Find components for the shorthand
-      var components = [];
-      var realComponents = [];
-      for (i = 0 ; i < componentsCount; i++) {
-        // Get property name
-        var pp = processable[prop].components[i];
-
-        if (componentsSoFar[prop].found[pp] && componentsSoFar[prop].found[pp].length) {
-          // We really found it
-          var foundRealComp = componentsSoFar[prop].found[pp][0];
-          components.push(foundRealComp);
-          if (foundRealComp.isReal !== false) {
-            realComponents.push(foundRealComp);
-          }
-        } else if (componentsSoFar[prop].lastShorthand) {
-          // It's defined in the previous shorthand
-          var c = componentsSoFar[prop].lastShorthand.components[i].clone(isImportant);
-          components.push(c);
-        } else {
-          // Couldn't find this component at all
-          return false;
-        }
-      }
-
-      if (realComponents.length === 0) {
-        // Couldn't find enough components, sorry
-        return false;
-      }
-
-      if (realComponents.length === componentsCount) {
-        // When all the components are from real values, only allow shorthanding if their understandability allows it
-        // This is the case when every component can override their default values, or when all of them use the same function
-
-        var canOverrideDefault = true;
-        var functionNameMatches = true;
-        var functionName;
-
-        for (var ci = 0; ci < realComponents.length; ci++) {
-          var rc = realComponents[ci];
-
-          if (!processable[rc.prop].canOverride(processable[rc.prop].defaultValue, rc.value)) {
-            canOverrideDefault = false;
-          }
-          var iop = rc.value.indexOf('(');
-          if (iop >= 0) {
-            var otherFunctionName = rc.value.substring(0, iop);
-            if (functionName)
-              functionNameMatches = functionNameMatches && otherFunctionName === functionName;
-            else
-              functionName = otherFunctionName;
-          }
-        }
-
-        if (!canOverrideDefault || !functionNameMatches)
-          return false;
-      }
-
-      // Compact the components into a shorthand
-      var compacted = processable[prop].putTogether(prop, components, isImportant);
-      if (!(compacted instanceof Array)) {
-        compacted = [compacted];
-      }
-
-      var compactedLength = Token.getDetokenizedLength(compacted);
-      var authenticLength = Token.getDetokenizedLength(realComponents);
-
-      if (realComponents.length === componentsCount || compactedLength < authenticLength || components.some(isHackValue)) {
-        compacted[0].isShorthand = true;
-        compacted[0].components = processable[prop].breakUp(compacted[0]);
-
-        // Mark the granular components for deletion
-        for (i = 0; i < realComponents.length; i++) {
-          realComponents[i].isMarkedForDeletion = true;
-        }
-
-        // Mark the position of the new shorthand
-        tokens[componentsSoFar[prop].shorthandPosition].replaceWith = compacted;
-
-        // Reinitialize the thing for further compacting
-        initSoFar(prop, compacted[0]);
-        for (i = 1; i < compacted.length; i++) {
-          addComponentSoFar(compacted[i]);
-        }
-
-        // Yes, we can keep the new shorthand!
-        return true;
-      }
-
-      return false;
-    };
-
-    // Tries to compact all properties currently in componentsSoFar
-    var compactAllSoFar = function () {
-      for (var i in componentsSoFar) {
-        if (componentsSoFar.hasOwnProperty(i)) {
-          while (compactSoFar(i)) { }
-        }
-      }
-    };
-
-    var i, token;
-
-    // Go through each token and collect components for each shorthand as we go on
-    for (i = 0; i < tokens.length; i++) {
-      token = tokens[i];
-      if (token.isMarkedForDeletion) {
-        continue;
-      }
-      if (!processable[token.prop]) {
-        // We don't know what it is, move on
-        continue;
-      }
-      if (processable[token.prop].isShorthand) {
-        // Found an instance of a full shorthand
-        // NOTE: we should NOT mix together tokens that come before and after the shorthands
-
-        if (token.isImportant === isImportant || (token.isImportant && !isImportant)) {
-          // Try to compact what we've found so far
-          while (compactSoFar(token.prop)) { }
-          // Reset
-          initSoFar(token.prop, token, true);
-        }
-
-        // TODO: when the old optimizer is removed, take care of this corner case:
-        //   div{background-color:#111;background-image:url(aaa);background:linear-gradient(aaa);background-repeat:no-repeat;background-position:1px 2px;background-attachment:scroll}
-        //   -> should not be shorthanded / minified at all because the result wouldn't be equivalent to the original in any browser
-      } else if (processable[token.prop].componentOf) {
-        // Found a component of a shorthand
-        if (token.isImportant === isImportant) {
-          // Same importantness
-          token.position = i;
-          addComponentSoFar(token, i);
-        } else if (!isImportant && token.isImportant) {
-          // Use importants for optimalization opportunities
-          // https://github.com/jakubpawlowicz/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
-  };
-
-})();
+// // Compacts the tokens by transforming properties into their shorthand notations when possible
+
+// module.exports = (function () {
+//   var isHackValue = function (t) { return t.value === '__hack'; };
+
+//   var compactShorthands = function(tokens, isImportant, processable, Token) {
+//     // Contains the components found so far, grouped by shorthand name
+//     var componentsSoFar = { };
+
+//     // Initializes a prop in componentsSoFar
+//     var initSoFar = function (shprop, last, clearAll) {
+//       var found = {};
+//       var shorthandPosition;
+
+//       if (!clearAll && componentsSoFar[shprop]) {
+//         for (var i = 0; i < processable[shprop].components.length; i++) {
+//           var prop = processable[shprop].components[i];
+//           found[prop] = [];
+
+//           if (!(componentsSoFar[shprop].found[prop]))
+//             continue;
+
+//           for (var ii = 0; ii < componentsSoFar[shprop].found[prop].length; ii++) {
+//             var comp = componentsSoFar[shprop].found[prop][ii];
+
+//             if (comp.trashed)
+//               continue;
+
+//             found[prop].push(comp);
+
+//             if (comp.position && (!shorthandPosition || comp.position < shorthandPosition))
+//               shorthandPosition = comp.position;
+//           }
+//         }
+//       }
+//       componentsSoFar[shprop] = {
+//         lastShorthand: last,
+//         found: found,
+//         shorthandPosition: shorthandPosition
+//       };
+//     };
+
+//     // Adds a component to componentsSoFar
+//     var addComponentSoFar = function (token, index) {
+//       var shprop = processable[token.name].componentOf;
+//       if (!componentsSoFar[shprop])
+//         initSoFar(shprop);
+//       if (!componentsSoFar[shprop].found[token.name])
+//         componentsSoFar[shprop].found[token.name] = [];
+
+//       // Add the newfound component to componentsSoFar
+//       componentsSoFar[shprop].found[token.name].push(token);
+
+//       if (!componentsSoFar[shprop].shorthandPosition && index) {
+//         // If the haven't decided on where the shorthand should go, put it in the place of this component
+//         componentsSoFar[shprop].shorthandPosition = index;
+//       }
+//     };
+
+//     // Tries to compact a prop in componentsSoFar
+//     var compactSoFar = function (prop) {
+//       var i;
+//       var componentsCount = processable[prop].components.length;
+
+//       // Check basics
+//       if (!componentsSoFar[prop] || !componentsSoFar[prop].found)
+//         return false;
+
+//       // Find components for the shorthand
+//       var components = [];
+//       var realComponents = [];
+//       for (i = 0 ; i < componentsCount; i++) {
+//         // Get property name
+//         var pp = processable[prop].components[i];
+
+//         if (componentsSoFar[prop].found[pp] && componentsSoFar[prop].found[pp].length) {
+//           // We really found it
+//           var foundRealComp = componentsSoFar[prop].found[pp][0];
+//           components.push(foundRealComp);
+//           if (foundRealComp.real !== false) {
+//             realComponents.push(foundRealComp);
+//           }
+//         } else if (componentsSoFar[prop].lastShorthand) {
+//           // It's defined in the previous shorthand
+//           var c = componentsSoFar[prop].lastShorthand.components[i].clone(isImportant);
+//           components.push(c);
+//         } else {
+//           // Couldn't find this component at all
+//           return false;
+//         }
+//       }
+
+//       if (realComponents.length === 0) {
+//         // Couldn't find enough components, sorry
+//         return false;
+//       }
+
+//       if (realComponents.length === componentsCount) {
+//         // When all the components are from real values, only allow shorthanding if their understandability allows it
+//         // This is the case when every component can override their default values, or when all of them use the same function
+
+//         var canOverrideDefault = true;
+//         var functionNameMatches = true;
+//         var functionName;
+
+//         for (var ci = 0; ci < realComponents.length; ci++) {
+//           var rc = realComponents[ci];
+
+//           if (!processable[rc.name].canOverride(processable[rc.name].defaultValue, rc.value)) {
+//             canOverrideDefault = false;
+//           }
+//           var iop = rc.value.indexOf('(');
+//           if (iop >= 0) {
+//             var otherFunctionName = rc.value.substring(0, iop);
+//             if (functionName)
+//               functionNameMatches = functionNameMatches && otherFunctionName === functionName;
+//             else
+//               functionName = otherFunctionName;
+//           }
+//         }
+
+//         if (!canOverrideDefault || !functionNameMatches)
+//           return false;
+//       }
+
+//       // Compact the components into a shorthand
+//       var compacted = processable[prop].putTogether(prop, components, isImportant);
+//       if (!(compacted instanceof Array)) {
+//         compacted = [compacted];
+//       }
+
+//       var compactedLength = Token.getDetokenizedLength(compacted);
+//       var authenticLength = Token.getDetokenizedLength(realComponents);
+
+//       if (realComponents.length === componentsCount || compactedLength < authenticLength || components.some(isHackValue)) {
+//         compacted[0].isShorthand = true;
+//         compacted[0].components = processable[prop].breakUp(compacted[0]);
+
+//         // Mark the granular components for deletion
+//         for (i = 0; i < realComponents.length; i++) {
+//           realComponents[i].isMarkedForDeletion = true;
+//         }
+
+//         // Mark the position of the new shorthand
+//         tokens[componentsSoFar[prop].shorthandPosition].replaceWith = compacted;
+
+//         // Reinitialize the thing for further compacting
+//         initSoFar(prop, compacted[0]);
+//         for (i = 1; i < compacted.length; i++) {
+//           addComponentSoFar(compacted[i]);
+//         }
+
+//         // Yes, we can keep the new shorthand!
+//         return true;
+//       }
+
+//       return false;
+//     };
+
+//     // Tries to compact all properties currently in componentsSoFar
+//     var compactAllSoFar = function () {
+//       for (var i in componentsSoFar) {
+//         if (componentsSoFar.hasOwnProperty(i)) {
+//           while (compactSoFar(i)) { }
+//         }
+//       }
+//     };
+
+//     var i, token;
+
+//     // Go through each token and collect components for each shorthand as we go on
+//     for (i = 0; i < tokens.length; i++) {
+//       token = tokens[i];
+//       if (token.trashed) {
+//         continue;
+//       }
+//       if (!processable[token.name]) {
+//         // We don't know what it is, move on
+//         continue;
+//       }
+//       if (processable[token.name].isShorthand) {
+//         // Found an instance of a full shorthand
+//         // NOTE: we should NOT mix together tokens that come before and after the shorthands
+
+//         if (token.important === isImportant || (token.important && !isImportant)) {
+//           // Try to compact what we've found so far
+//           while (compactSoFar(token.name)) { }
+//           // Reset
+//           initSoFar(token.name, token, true);
+//         }
+
+//         // TODO: when the old optimizer is removed, take care of this corner case:
+//         //   div{background-color:#111;background-image:url(aaa);background:linear-gradient(aaa);background-repeat:no-repeat;background-position:1px 2px;background-attachment:scroll}
+//         //   -> should not be shorthanded / minified at all because the result wouldn't be equivalent to the original in any browser
+//       } else if (processable[token.name].componentOf) {
+//         // Found a component of a shorthand
+//         if (token.important === isImportant) {
+//           // Same importantness
+//           token.position = i;
+//           addComponentSoFar(token, i);
+//         } else if (!isImportant && token.important) {
+//           // Use importants for optimalization opportunities
+//           // https://github.com/jakubpawlowicz/clean-css/issues/184
+//           var importantTrickComp = new Token(token.name, token.value, isImportant);
+//           importantTrickComp.irrelevant = true;
+//           importantTrickComp.real = 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.trashed) {
+//         result.push(token);
+//       }
+
+//       token.trashed = false;
+//       token.replaceWith = null;
+//     }
+
+//     return result;
+//   };
+
+//   return {
+//     compactShorthands: compactShorthands
+//   };
+
+// })();
diff --git a/lib/properties/token.js b/lib/properties/token.js
deleted file mode 100644 (file)
index 102f9b8..0000000
+++ /dev/null
@@ -1,186 +0,0 @@
-
-// 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;
-    Token.prototype.metadata = null;
-
-    // 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[0].indexOf(':');
-
-      if (colonPos < 0) {
-        // This property doesn't have a colon, it's invalid. Let's keep it intact anyway.
-        return new Token('', fullProp[0]);
-      }
-
-      // Parse parts of the property
-      var prop = fullProp[0].substr(0, colonPos).trim();
-      var value = fullProp[0].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;
-      }
-
-      result.metadata = fullProp.metadata;
-
-      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.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];
-      }
-
-      var list = [];
-
-      // 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;
-        }
-
-        // FIXME: the check should be gone with #407
-        var property = t.prop === '' && t.value.indexOf('__ESCAPED_') === 0 ?
-          t.value :
-          t.prop + ':' + t.value + (t.isImportant ? important : '');
-
-        // FIXME: to be fixed with #429
-        property = property.replace(/\) ([^\+\-\/\*])/g, ')$1');
-
-        list.push([property]);
-      }
-
-      return list;
-    };
-
-    // 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
-  };
-
-})();
index e296400..b06b698 100644 (file)
-
 // Validates various CSS property values
 
 var Splitter = require('../utils/splitter');
 
-module.exports = (function () {
-  // Regexes used for stuff
-  var widthKeywords = ['thin', 'thick', 'medium', 'inherit', 'initial'];
-  var allUnits = ['px', '%', 'em', 'rem', 'in', 'cm', 'mm', 'ex', 'pt', 'pc', 'vw', 'vh', 'vmin', 'vmax'];
-  var cssUnitRegexStr = '(\\-?\\.?\\d+\\.?\\d*(' + allUnits.join('|') + '|)|auto|inherit)';
-  var cssCalcRegexStr = '(\\-moz\\-|\\-webkit\\-)?calc\\([^\\)]+\\)';
-  var cssFunctionNoVendorRegexStr = '[A-Z]+(\\-|[A-Z]|[0-9])+\\(([A-Z]|[0-9]|\\ |\\,|\\#|\\+|\\-|\\%|\\.|\\(|\\))*\\)';
-  var cssFunctionVendorRegexStr = '\\-(\\-|[A-Z]|[0-9])+\\(([A-Z]|[0-9]|\\ |\\,|\\#|\\+|\\-|\\%|\\.|\\(|\\))*\\)';
-  var cssVariableRegexStr = 'var\\(\\-\\-[^\\)]+\\)';
-  var cssFunctionAnyRegexStr = '(' + cssVariableRegexStr + '|' + cssFunctionNoVendorRegexStr + '|' + cssFunctionVendorRegexStr + ')';
-  var cssUnitOrCalcRegexStr = '(' + cssUnitRegexStr + '|' + cssCalcRegexStr + ')';
-  var cssUnitAnyRegexStr = '(none|' + widthKeywords.join('|') + '|' + cssUnitRegexStr + '|' + cssVariableRegexStr + '|' + cssFunctionNoVendorRegexStr + '|' + cssFunctionVendorRegexStr + ')';
-
-  var cssFunctionNoVendorRegex = new RegExp('^' + cssFunctionNoVendorRegexStr + '$', 'i');
-  var cssFunctionVendorRegex = new RegExp('^' + cssFunctionVendorRegexStr + '$', 'i');
-  var cssVariableRegex = new RegExp('^' + cssVariableRegexStr + '$', 'i');
-  var cssFunctionAnyRegex = new RegExp('^' + cssFunctionAnyRegexStr + '$', 'i');
-  var cssUnitRegex = new RegExp('^' + cssUnitRegexStr + '$', 'i');
-  var cssUnitOrCalcRegex = new RegExp('^' + cssUnitOrCalcRegexStr + '$', 'i');
-  var cssUnitAnyRegex = new RegExp('^' + cssUnitAnyRegexStr + '$', 'i');
-
-  var backgroundRepeatKeywords = ['repeat', 'no-repeat', 'repeat-x', 'repeat-y', 'inherit'];
-  var backgroundAttachmentKeywords = ['inherit', 'scroll', 'fixed', 'local'];
-  var backgroundPositionKeywords = ['center', 'top', 'bottom', 'left', 'right'];
-  var backgroundSizeKeywords = ['contain', 'cover'];
-  var backgroundBoxKeywords = ['border-box', 'content-box', 'padding-box'];
-  var listStyleTypeKeywords = ['armenian', 'circle', 'cjk-ideographic', 'decimal', 'decimal-leading-zero', 'disc', 'georgian', 'hebrew', 'hiragana', 'hiragana-iroha', 'inherit', 'katakana', 'katakana-iroha', 'lower-alpha', 'lower-greek', 'lower-latin', 'lower-roman', 'none', 'square', 'upper-alpha', 'upper-latin', 'upper-roman'];
-  var listStylePositionKeywords = ['inside', 'outside', 'inherit'];
-  var outlineStyleKeywords = ['auto', 'inherit', 'hidden', 'none', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'];
-
-  var compatibleCssUnitRegex;
-  var compatibleCssUnitAnyRegex;
-
-  var validator = {
-    // FIXME: we need a proper OO here
-    setCompatibility: function (compatibility) {
-      if (compatibility.units.rem) {
-        compatibleCssUnitRegex = cssUnitRegex;
-        compatibleCssUnitAnyRegex = cssUnitAnyRegex;
-        return;
-      }
-
-      var validUnits = allUnits.slice(0).filter(function (value) {
-        return value != 'rem';
-      });
-
-      var compatibleCssUnitRegexStr = '(\\-?\\.?\\d+\\.?\\d*(' + validUnits.join('|') + ')|auto|inherit)';
-      compatibleCssUnitRegex = new RegExp('^' + compatibleCssUnitRegexStr + '$', 'i');
-      compatibleCssUnitAnyRegex = new RegExp('^(none|' + widthKeywords.join('|') + '|' + compatibleCssUnitRegexStr + '|' + cssVariableRegexStr + '|' + cssFunctionNoVendorRegexStr + '|' + cssFunctionVendorRegexStr + ')$', 'i');
-    },
-
-    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) {
-      // 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));
-    },
-    isValidVariable: function(s) {
-      return cssVariableRegex.test(s);
-    },
-    isValidColor: function (s) {
-      return validator.isValidNamedColor(s) || validator.isValidHexColor(s) || validator.isValidRgbaColor(s) || validator.isValidHslaColor(s) || validator.isValidVariable(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 cssUnitAnyRegex.test(s);
-    },
-    isValidUnitWithoutFunction: function (s) {
-      return cssUnitRegex.test(s);
-    },
-    isValidAndCompatibleUnit: function (s) {
-      return compatibleCssUnitAnyRegex.test(s);
-    },
-    isValidAndCompatibleUnitWithoutFunction: function (s) {
-      return compatibleCssUnitRegex.test(s);
-    },
-    isValidFunctionWithoutVendorPrefix: function (s) {
-      return cssFunctionNoVendorRegex.test(s);
-    },
-    isValidFunctionWithVendorPrefix: function (s) {
-      return cssFunctionVendorRegex.test(s);
-    },
-    isValidFunction: function (s) {
-      return cssFunctionAnyRegex.test(s);
-    },
-    isValidBackgroundRepeat: function (s) {
-      return backgroundRepeatKeywords.indexOf(s) >= 0 || validator.isValidVariable(s);
-    },
-    isValidBackgroundAttachment: function (s) {
-      return backgroundAttachmentKeywords.indexOf(s) >= 0 || validator.isValidVariable(s);
-    },
-    isValidBackgroundBox: function (s) {
-      return backgroundBoxKeywords.indexOf(s) >= 0 || validator.isValidVariable(s);
-    },
-    isValidBackgroundPositionPart: function (s) {
-      return backgroundPositionKeywords.indexOf(s) >= 0 || cssUnitOrCalcRegex.test(s) || validator.isValidVariable(s);
-    },
-    isValidBackgroundPosition: function (s) {
-      if (s === 'inherit')
-        return true;
-
-      var parts = s.split(' ');
-      for (var i = 0, l = parts.length; i < l; i++) {
-        if (parts[i] === '')
-          continue;
-        if (validator.isValidBackgroundPositionPart(parts[i]) || validator.isValidVariable(parts[i]))
-          continue;
-
-        return false;
-      }
-
-      return true;
-    },
-    isValidBackgroundSizePart: function(s) {
-      return backgroundSizeKeywords.indexOf(s) >= 0 || cssUnitRegex.test(s) || validator.isValidVariable(s);
-    },
-    isValidBackgroundPositionAndSize: function(s) {
-      if (s.indexOf('/') < 0)
-        return false;
-
-      var twoParts = new Splitter('/').split(s);
-      return validator.isValidBackgroundSizePart(twoParts.pop()) && validator.isValidBackgroundPositionPart(twoParts.pop());
-    },
-    isValidListStyleType: function (s) {
-      return listStyleTypeKeywords.indexOf(s) >= 0 || validator.isValidVariable(s);
-    },
-    isValidListStylePosition: function (s) {
-      return listStylePositionKeywords.indexOf(s) >= 0 || validator.isValidVariable(s);
-    },
-    isValidOutlineColor: function (s) {
-      return s === 'invert' || validator.isValidColor(s) || validator.isValidVendorPrefixedValue(s);
-    },
-    isValidOutlineStyle: function (s) {
-      return outlineStyleKeywords.indexOf(s) >= 0 || validator.isValidVariable(s);
-    },
-    isValidOutlineWidth: function (s) {
-      return validator.isValidUnit(s) || widthKeywords.indexOf(s) >= 0 || validator.isValidVariable(s);
-    },
-    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;
-    }
-  };
-
-  return validator;
-})();
+var widthKeywords = ['thin', 'thick', 'medium', 'inherit', 'initial'];
+var allUnits = ['px', '%', 'em', 'rem', 'in', 'cm', 'mm', 'ex', 'pt', 'pc', 'vw', 'vh', 'vmin', 'vmax'];
+var cssUnitRegexStr = '(\\-?\\.?\\d+\\.?\\d*(' + allUnits.join('|') + '|)|auto|inherit)';
+var cssCalcRegexStr = '(\\-moz\\-|\\-webkit\\-)?calc\\([^\\)]+\\)';
+var cssFunctionNoVendorRegexStr = '[A-Z]+(\\-|[A-Z]|[0-9])+\\(([A-Z]|[0-9]|\\ |\\,|\\#|\\+|\\-|\\%|\\.|\\(|\\))*\\)';
+var cssFunctionVendorRegexStr = '\\-(\\-|[A-Z]|[0-9])+\\(([A-Z]|[0-9]|\\ |\\,|\\#|\\+|\\-|\\%|\\.|\\(|\\))*\\)';
+var cssVariableRegexStr = 'var\\(\\-\\-[^\\)]+\\)';
+var cssFunctionAnyRegexStr = '(' + cssVariableRegexStr + '|' + cssFunctionNoVendorRegexStr + '|' + cssFunctionVendorRegexStr + ')';
+var cssUnitOrCalcRegexStr = '(' + cssUnitRegexStr + '|' + cssCalcRegexStr + ')';
+var cssUnitAnyRegexStr = '(none|' + widthKeywords.join('|') + '|' + cssUnitRegexStr + '|' + cssVariableRegexStr + '|' + cssFunctionNoVendorRegexStr + '|' + cssFunctionVendorRegexStr + ')';
+
+var cssFunctionNoVendorRegex = new RegExp('^' + cssFunctionNoVendorRegexStr + '$', 'i');
+var cssFunctionVendorRegex = new RegExp('^' + cssFunctionVendorRegexStr + '$', 'i');
+var cssVariableRegex = new RegExp('^' + cssVariableRegexStr + '$', 'i');
+var cssFunctionAnyRegex = new RegExp('^' + cssFunctionAnyRegexStr + '$', 'i');
+var cssUnitRegex = new RegExp('^' + cssUnitRegexStr + '$', 'i');
+var cssUnitOrCalcRegex = new RegExp('^' + cssUnitOrCalcRegexStr + '$', 'i');
+var cssUnitAnyRegex = new RegExp('^' + cssUnitAnyRegexStr + '$', 'i');
+
+var backgroundRepeatKeywords = ['repeat', 'no-repeat', 'repeat-x', 'repeat-y', 'inherit'];
+var backgroundAttachmentKeywords = ['inherit', 'scroll', 'fixed', 'local'];
+var backgroundPositionKeywords = ['center', 'top', 'bottom', 'left', 'right'];
+var backgroundSizeKeywords = ['contain', 'cover'];
+var backgroundBoxKeywords = ['border-box', 'content-box', 'padding-box'];
+var styleKeywords = ['auto', 'inherit', 'hidden', 'none', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'];
+var listStyleTypeKeywords = ['armenian', 'circle', 'cjk-ideographic', 'decimal', 'decimal-leading-zero', 'disc', 'georgian', 'hebrew', 'hiragana', 'hiragana-iroha', 'inherit', 'katakana', 'katakana-iroha', 'lower-alpha', 'lower-greek', 'lower-latin', 'lower-roman', 'none', 'square', 'upper-alpha', 'upper-latin', 'upper-roman'];
+var listStylePositionKeywords = ['inside', 'outside', 'inherit'];
+
+// var compatibleCssUnitRegex;
+// var compatibleCssUnitAnyRegex;
+
+//   var validator = {
+//     // FIXME: we need a proper OO here
+//     setCompatibility: function (compatibility) {
+//       if (compatibility.units.rem) {
+//         compatibleCssUnitRegex = cssUnitRegex;
+//         compatibleCssUnitAnyRegex = cssUnitAnyRegex;
+//         return;
+//       }
+
+//       var validUnits = allUnits.slice(0).filter(function (value) {
+//         return value != 'rem';
+//       });
+
+//       var compatibleCssUnitRegexStr = '(\\-?\\.?\\d+\\.?\\d*(' + validUnits.join('|') + ')|auto|inherit)';
+//       compatibleCssUnitRegex = new RegExp('^' + compatibleCssUnitRegexStr + '$', 'i');
+//       compatibleCssUnitAnyRegex = new RegExp('^(none|' + widthKeywords.join('|') + '|' + compatibleCssUnitRegexStr + '|' + cssVariableRegexStr + '|' + cssFunctionNoVendorRegexStr + '|' + cssFunctionVendorRegexStr + ')$', 'i');
+//     }
+
+function isValidHexColor(s) {
+  return (s.length === 4 || s.length === 7) && s[0] === '#';
+}
+function isValidRgbaColor(s) {
+  s = s.split(' ').join('');
+  return s.length > 0 && s.indexOf('rgba(') === 0 && s.indexOf(')') === s.length - 1;
+}
+function isValidHslaColor(s) {
+  s = s.split(' ').join('');
+  return s.length > 0 && s.indexOf('hsla(') === 0 && s.indexOf(')') === s.length - 1;
+}
+function isValidNamedColor(s) {
+  // 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));
+}
+function isValidVariable(s) {
+  return cssVariableRegex.test(s);
+}
+function isValidColor(s) {
+  return isValidNamedColor(s) || isValidHexColor(s) || isValidRgbaColor(s) || isValidHslaColor(s) || isValidVariable(s) || isValidVendorPrefixedValue(s);
+}
+function isValidUrl(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;
+}
+function isValidUnit(s) {
+  return cssUnitAnyRegex.test(s);
+}
+function isValidUnitWithoutFunction(s) {
+  return cssUnitRegex.test(s);
+}
+function isValidAndCompatibleUnit(s) {
+  return cssUnitAnyRegex.test(s);
+}
+function isValidAndCompatibleUnitWithoutFunction(s) {
+  return cssUnitRegex.test(s);
+}
+function isValidFunctionWithoutVendorPrefix(s) {
+  return cssFunctionNoVendorRegex.test(s);
+}
+function isValidFunctionWithVendorPrefix(s) {
+  return cssFunctionVendorRegex.test(s);
+}
+function isValidFunction(s) {
+  return cssFunctionAnyRegex.test(s);
+}
+function isValidBackgroundRepeat(s) {
+  return backgroundRepeatKeywords.indexOf(s) >= 0 || isValidVariable(s);
+}
+function isValidBackgroundAttachment(s) {
+  return backgroundAttachmentKeywords.indexOf(s) >= 0 || isValidVariable(s);
+}
+function isValidBackgroundBox(s) {
+  return backgroundBoxKeywords.indexOf(s) >= 0 || isValidVariable(s);
+}
+function isValidBackgroundPositionPart(s) {
+  return backgroundPositionKeywords.indexOf(s) >= 0 || cssUnitOrCalcRegex.test(s) || isValidVariable(s);
+}
+function isValidBackgroundPosition(s) {
+  if (s === 'inherit')
+    return true;
+
+  var parts = s.split(' ');
+  for (var i = 0, l = parts.length; i < l; i++) {
+    if (parts[i] === '')
+      continue;
+    if (isValidBackgroundPositionPart(parts[i]) || isValidVariable(parts[i]))
+      continue;
+
+    return false;
+  }
+
+  return true;
+}
+function isValidBackgroundSizePart(s) {
+  return backgroundSizeKeywords.indexOf(s) >= 0 || cssUnitRegex.test(s) || isValidVariable(s);
+}
+function isValidBackgroundPositionAndSize(s) {
+  if (s.indexOf('/') < 0)
+    return false;
+
+  var twoParts = new Splitter('/').split(s);
+  return isValidBackgroundSizePart(twoParts.pop()) && isValidBackgroundPositionPart(twoParts.pop());
+}
+function isValidListStyleType(s) {
+  return listStyleTypeKeywords.indexOf(s) >= 0 || isValidVariable(s);
+}
+function isValidListStylePosition(s) {
+  return listStylePositionKeywords.indexOf(s) >= 0 || isValidVariable(s);
+}
+function isValidStyle(s) {
+  return styleKeywords.indexOf(s) >= 0 || isValidVariable(s);
+}
+function isValidWidth(s) {
+  return isValidUnit(s) || widthKeywords.indexOf(s) >= 0 || isValidVariable(s);
+}
+function isValidVendorPrefixedValue(s) {
+  return /^-([A-Za-z0-9]|-)*$/gi.test(s);
+}
+function areSameFunction(a, b) {
+  if (!isValidFunction(a) || !isValidFunction(b))
+    return false;
+
+  var f1name = a.substring(0, a.indexOf('('));
+  var f2name = b.substring(0, b.indexOf('('));
+
+  return f1name === f2name;
+}
+
+module.exports = {
+  isValidHexColor: isValidHexColor,
+  isValidRgbaColor: isValidRgbaColor,
+  isValidHslaColor: isValidHslaColor,
+  isValidNamedColor: isValidNamedColor,
+  isValidVariable: isValidVariable,
+  isValidColor: isValidColor,
+  isValidUrl: isValidUrl,
+  isValidUnit: isValidUnit,
+  isValidUnitWithoutFunction: isValidUnitWithoutFunction,
+  isValidAndCompatibleUnit: isValidAndCompatibleUnit,
+  isValidAndCompatibleUnitWithoutFunction: isValidAndCompatibleUnitWithoutFunction,
+  isValidFunctionWithoutVendorPrefix: isValidFunctionWithoutVendorPrefix,
+  isValidFunctionWithVendorPrefix: isValidFunctionWithVendorPrefix,
+  isValidFunction: isValidFunction,
+  isValidBackgroundRepeat: isValidBackgroundRepeat,
+  isValidBackgroundAttachment: isValidBackgroundAttachment,
+  isValidBackgroundBox: isValidBackgroundBox,
+  isValidBackgroundPositionPart: isValidBackgroundPositionPart,
+  isValidBackgroundPosition: isValidBackgroundPosition,
+  isValidBackgroundSizePart: isValidBackgroundSizePart,
+  isValidBackgroundPositionAndSize: isValidBackgroundPositionAndSize,
+  isValidListStyleType: isValidListStyleType,
+  isValidListStylePosition: isValidListStylePosition,
+  isValidStyle: isValidStyle,
+  isValidWidth: isValidWidth,
+  isValidVendorPrefixedValue: isValidVendorPrefixedValue,
+  areSameFunction: areSameFunction
+};
diff --git a/lib/properties/wrap-for-optimizing.js b/lib/properties/wrap-for-optimizing.js
new file mode 100644 (file)
index 0000000..265043a
--- /dev/null
@@ -0,0 +1,46 @@
+function wrapAll(properties) {
+  var wrapped = [];
+
+  for (var i = properties.length - 1; i >= 0; i--) {
+    if (typeof properties[i][0] == 'string')
+      continue;
+
+    var single = wrapSingle(properties[i]);
+    single.all = properties;
+    single.position = i;
+    wrapped.unshift(single);
+  }
+
+  return wrapped;
+}
+
+function isMultiplex(property) {
+  for (var i = 1, l = property.length; i < l; i++) {
+    if (property[i][0] == ',' || property[i][0] == '/')
+      return true;
+  }
+
+  return false;
+}
+
+function wrapSingle(property) {
+  return {
+    components: [],
+    dirty: false,
+    hack: property[0][2],
+    important: property[0][1],
+    irrelevant: false,
+    name: property[0][0],
+    multiplex: property.length > 2 ? isMultiplex(property) : false,
+    position: 0,
+    real: true,
+    shorthand: false,
+    unused: property.length < 2,
+    value: property.slice(1)
+  };
+}
+
+module.exports = {
+  all: wrapAll,
+  single: wrapSingle
+};
similarity index 71%
rename from lib/properties/extractor.js
rename to lib/selectors/extractor.js
index ac131ac..877fc68 100644 (file)
@@ -2,29 +2,32 @@
 // IMPORTANT: Mind Token class and this code is not related!
 // Properties will be tokenized in one step, see #429
 
+var stringifySelector = require('../utils/stringify-tokens').selector;
+var stringifyValue = require('../utils/stringify-tokens').value;
+
 function extract(token) {
   var properties = [];
 
   if (token[0] == 'selector') {
-    // TODO: stringifySelector
-    var inSimpleSelector = !/[\.\+#>~\s]/.test(token[1].join(','));
+    var inSimpleSelector = !/[\.\+#>~\s]/.test(stringifySelector(token[1]));
     for (var i = 0, l = token[2].length; i < l; i++) {
-      var property = token[2][i][0];
+      var property = token[2][i];
+
       if (property.indexOf('__ESCAPED') === 0)
         continue;
 
-      var splitAt = property.indexOf(':');
-      var name = property.substring(0, splitAt);
-      if (!name)
+      var name = token[2][i][0][0];
+      if (name.length === 0)
         continue;
 
-      var nameRoot = findNameRoot(name);
+      var value = stringifyValue(token[2][i]);
 
       properties.push([
         name,
-        property.substring(splitAt + 1),
-        nameRoot,
+        value,
+        findNameRoot(name),
         token[2][i],
+        name + ':' + value,
         token[1],
         inSimpleSelector
       ]);
diff --git a/lib/selectors/metadata.js b/lib/selectors/metadata.js
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/selectors/optimization-metadata.js b/lib/selectors/optimization-metadata.js
new file mode 100644 (file)
index 0000000..b05296c
--- /dev/null
@@ -0,0 +1,46 @@
+// TODO: we should wrap it under `wrap for optimizing`
+
+var BACKSLASH_HACK = '\\9';
+var IMPORTANT = '!important';
+var STAR_HACK = '*';
+var UNDERSCORE_HACK = '_';
+
+function addOptimizationMetadata(tokens) {
+  for (var i = tokens.length - 1; i >= 0; i--) {
+    var token = tokens[i];
+
+    switch (token[0]) {
+      case 'selector':
+        addToProperties(token[2]);
+        break;
+      case 'block':
+        addOptimizationMetadata(token[2]);
+        break;
+    }
+  }
+}
+
+function addToProperties(properties) {
+  for (var i = properties.length - 1; i >= 0; i--) {
+    if (typeof properties[i] != 'string')
+      addToProperty(properties[i]);
+  }
+}
+
+function addToProperty(property) {
+  var name = property[0][0];
+  var lastValue = property[property.length - 1];
+  var isImportant = lastValue[0].indexOf(IMPORTANT) > 0;
+  var isHack = name[0] == UNDERSCORE_HACK ||
+    name[0] == STAR_HACK ||
+    lastValue[0].indexOf(BACKSLASH_HACK) > 0 && lastValue[0].indexOf(BACKSLASH_HACK) == lastValue[0].length - BACKSLASH_HACK.length;
+
+  // TODO: this should be done at tokenization step
+  // with adding importance info
+  if (isImportant)
+    lastValue[0] = lastValue[0].substring(0, lastValue[0].length - IMPORTANT.length);
+
+  property[0].splice(1, 0, isImportant, isHack);
+}
+
+module.exports = addOptimizationMetadata;
index b2a46fa..a7fa21f 100644 (file)
@@ -1,6 +1,7 @@
 var Tokenizer = require('./tokenizer');
 var SimpleOptimizer = require('./optimizers/simple');
 var AdvancedOptimizer = require('./optimizers/advanced');
+var addOptimizationMetadata = require('./optimization-metadata');
 
 function SelectorsOptimizer(options, context) {
   this.options = options || {};
@@ -10,6 +11,8 @@ function SelectorsOptimizer(options, context) {
 SelectorsOptimizer.prototype.process = function (data, stringifier) {
   var tokens = new Tokenizer(this.context, this.options.sourceMap).toTokens(data);
 
+  addOptimizationMetadata(tokens);
+
   new SimpleOptimizer(this.options).optimize(tokens);
   if (this.options.advanced)
     new AdvancedOptimizer(this.options, this.context).optimize(tokens);
index 98cb937..4502de8 100644 (file)
@@ -1,15 +1,14 @@
-var PropertyOptimizer = require('../../properties/optimizer');
+var optimizeProperties = require('../../properties/optimizer');
 var CleanUp = require('./clean-up');
 
-var extractProperties = require('../../properties/extractor');
-var canReorder = require('../../properties/reorderable').canReorder;
-var canReorderSingle = require('../../properties/reorderable').canReorderSingle;
+var extractProperties = require('../extractor');
+var canReorder = require('../reorderable').canReorder;
+var canReorderSingle = require('../reorderable').canReorderSingle;
 var stringifyBody = require('../../utils/stringify-tokens').body;
 var stringifySelector = require('../../utils/stringify-tokens').selector;
 
-function AdvancedOptimizer(options, context) {
+function AdvancedOptimizer(options) {
   this.options = options;
-  this.propertyOptimizer = new PropertyOptimizer(this.options, context);
 }
 
 function unsafeSelector(value) {
@@ -75,7 +74,8 @@ AdvancedOptimizer.prototype.mergeAdjacent = function (tokens) {
 
     if (lastToken[0] == 'selector' && stringifySelector(token[1]) == stringifySelector(lastToken[1])) {
       var joinAt = [lastToken[2].length];
-      lastToken[2] = this.propertyOptimizer.process(token[1], lastToken[2].concat(token[2]), joinAt, true);
+      Array.prototype.push.apply(lastToken[2], token[2]);
+      optimizeProperties(token[1], lastToken[2], joinAt, this.options);
       token[2] = [];
     } else if (lastToken[0] == 'selector' && stringifyBody(token[2]) == stringifyBody(lastToken[2]) &&
         !this.isSpecial(stringifySelector(token[1])) && !this.isSpecial(stringifySelector(lastToken[1]))) {
@@ -96,6 +96,8 @@ AdvancedOptimizer.prototype.reduceNonAdjacent = function (tokens) {
 
     if (token[0] != 'selector')
       continue;
+    if (token[2].length === 0)
+      continue;
 
     var selectorAsString = stringifySelector(token[1]);
     var isComplexAndNotSpecial = token[1].length > 1 && !this.isSpecial(selectorAsString);
@@ -219,19 +221,19 @@ AdvancedOptimizer.prototype.reduceSelector = function (tokens, selector, data, o
       joinsAt.push((joinsAt[j - 1] || 0) + bodiesAsList[j].length);
   }
 
-  var optimizedBody = this.propertyOptimizer.process(selector, bodies, joinsAt, false);
+  optimizeProperties(selector, bodies, joinsAt, this.options);
 
   var processedCount = processedTokens.length;
-  var propertyIdx = optimizedBody.length - 1;
+  var propertyIdx = bodies.length - 1;
   var tokenIdx = processedCount - 1;
 
   while (tokenIdx >= 0) {
-     if ((tokenIdx === 0 || (optimizedBody[propertyIdx] && stringifyBody(bodiesAsList[tokenIdx]).indexOf(optimizedBody[propertyIdx]) > -1)) && propertyIdx > -1) {
+     if ((tokenIdx === 0 || (bodies[propertyIdx] && bodiesAsList[tokenIdx].indexOf(bodies[propertyIdx]) > -1)) && propertyIdx > -1) {
       propertyIdx--;
       continue;
     }
 
-    var newBody = optimizedBody.splice(propertyIdx + 1);
+    var newBody = bodies.splice(propertyIdx + 1);
     options.callback(tokens[processedTokens[tokenIdx]], newBody, processedCount, tokenIdx);
 
     tokenIdx--;
@@ -292,8 +294,14 @@ AdvancedOptimizer.prototype.mergeNonAdjacentBySelector = function (tokens) {
         }
 
         var joinAt = topToBottom ? [target[2].length] : [moved[2].length];
-        var joinedBodies = topToBottom ? moved[2].concat(target[2]) : target[2].concat(moved[2]);
-        target[2] = this.propertyOptimizer.process(target[1], joinedBodies, joinAt, true);
+        if (topToBottom) {
+          Array.prototype.push.apply(moved[2], target[2]);
+          target[2] = moved[2];
+        } else {
+          Array.prototype.push.apply(target[2], moved[2]);
+        }
+
+        optimizeProperties(target[1], target[2], joinAt, this.options);
         moved[2] = [];
       }
     }
@@ -401,7 +409,7 @@ AdvancedOptimizer.prototype.restructure = function (tokens) {
   function shortenIfPossible(position, movedProperty) {
     var name = movedProperty[0];
     var value = movedProperty[1];
-    var key = movedProperty[3][0];
+    var key = movedProperty[4];
     var valueSize = name.length + value.length + 1;
     var allSelectors = [];
     var qualifiedTokens = [];
@@ -466,7 +474,9 @@ AdvancedOptimizer.prototype.restructure = function (tokens) {
         for (k = 0, m = properties.length; k < m; k++) {
           var property = properties[k];
 
-          if (mergeableToken[2][j][0] === property[3][0]) {
+          var mergeablePropertyName = mergeableToken[2][j][0][0];
+          var propertyName = property[3][0][0];
+          if (mergeablePropertyName === propertyName) {
             mergeableToken[2].splice(j, 1);
             break;
           }
@@ -483,7 +493,7 @@ AdvancedOptimizer.prototype.restructure = function (tokens) {
   }
 
   function dropPropertiesAt(position, movedProperty) {
-    var key = movedProperty[3][0];
+    var key = movedProperty[4];
 
     if (movableTokens[key] && movableTokens[key].length > 1)
       shortenIfPossible(position, movedProperty);
@@ -496,7 +506,7 @@ AdvancedOptimizer.prototype.restructure = function (tokens) {
 
     for (var i = propertiesAndMergableTokens.length - 1; i >= 0; i--) {
       property = propertiesAndMergableTokens[i][0];
-      var fullValue = property[3][0];
+      var fullValue = property[4];
       valueSize += fullValue.length + (i > 0 ? 1 : 0);
 
       properties.push(property);
@@ -521,7 +531,7 @@ AdvancedOptimizer.prototype.restructure = function (tokens) {
       property = properties[i];
       var index = movedProperties.indexOf(property);
 
-      delete movableTokens[property[3]];
+      delete movableTokens[property[4]];
 
       if (index > -1 && movedToBeDropped.indexOf(index) == -1)
         movedToBeDropped.push(index);
@@ -569,7 +579,7 @@ AdvancedOptimizer.prototype.restructure = function (tokens) {
         if (movedToBeDropped.indexOf(k) == -1 && !canReorderSingle(property, movedProperty)) {
           dropPropertiesAt(i + 1, movedProperty);
           movedToBeDropped.push(k);
-          delete movableTokens[movedProperty[3][0]];
+          delete movableTokens[movedProperty[4]];
         }
 
         if (!movedSameProperty)
@@ -579,7 +589,7 @@ AdvancedOptimizer.prototype.restructure = function (tokens) {
       if (!isSelector || unmovableInCurrentToken.indexOf(j) > -1)
         continue;
 
-      var key = property[3][0];
+      var key = property[4];
       movableTokens[key] = movableTokens[key] || [];
       movableTokens[key].push(token);
 
@@ -665,16 +675,16 @@ AdvancedOptimizer.prototype.removeEmpty = function (tokens) {
   }
 };
 
-function optimizeProperties(tokens, propertyOptimizer) {
+function recursivelyOptimizeProperties(tokens, options) {
   for (var i = 0, l = tokens.length; i < l; i++) {
     var token = tokens[i];
 
     switch (token[0]) {
       case 'selector':
-        token[2] = propertyOptimizer.process(token[1], token[2], false, true);
+        optimizeProperties(token[1], token[2], false, options);
         break;
       case 'block':
-        optimizeProperties(token[2], propertyOptimizer);
+        recursivelyOptimizeProperties(token[2], options);
     }
   }
 }
@@ -690,7 +700,7 @@ AdvancedOptimizer.prototype.optimize = function (tokens) {
       }
     });
 
-    optimizeProperties(tokens, self.propertyOptimizer);
+    recursivelyOptimizeProperties(tokens, self.options);
 
     self.removeDuplicates(tokens);
     self.mergeAdjacent(tokens);
index 45bf760..66fabb9 100644 (file)
@@ -5,8 +5,6 @@ var RGB = require('../../colors/rgb');
 var HSL = require('../../colors/hsl');
 var HexNameShortener = require('../../colors/hex-name-shortener');
 
-var processable = require('../../properties/processable');
-
 var DEFAULT_ROUNDING_PRECISION = 2;
 var CHARSET_TOKEN = '@charset';
 var CHARSET_REGEXP = new RegExp('^' + CHARSET_TOKEN, 'i');
@@ -31,35 +29,6 @@ var valueMinifiers = {
   'background': function (value) {
     return value == 'none' || value == 'transparent' ? '0 0' : value;
   },
-  'border-*-radius': function (value) {
-    if (value.indexOf('/') == -1)
-      return value;
-
-    var parts = value.split(/\s*\/\s*/);
-    if (parts[0] == parts[1])
-      return parts[0];
-    else
-      return parts[0] + '/' + parts[1];
-  },
-  'filter': function (value) {
-    if (value.indexOf('DXImageTransform') === value.lastIndexOf('DXImageTransform')) {
-      value = value.replace(/progid:DXImageTransform\.Microsoft\.(Alpha|Chroma)(\W)/, function (match, filter, suffix) {
-        return filter.toLowerCase() + suffix;
-      });
-    }
-
-    return value
-      .replace(/,(\S)/g, ', $1')
-      .replace(/ ?= ?/g, '=');
-  },
-  'font': function (value) {
-    var parts = value.split(' ');
-
-    if (parts[1] != 'normal' && parts[1] != 'bold' && !/^[1-9]00/.test(parts[1]))
-      parts[0] = this['font-weight'](parts[0]);
-
-    return parts.join(' ');
-  },
   'font-weight': function (value) {
     if (value == 'normal')
       return '400';
@@ -73,17 +42,11 @@ var valueMinifiers = {
   }
 };
 
-function isNegative(value) {
-  var parts = new Splitter(',').split(value);
-  for (var i = 0, l = parts.length; i < l; i++) {
-    if (parts[i][0] == '-' && parseFloat(parts[i]) < 0)
-      return true;
-  }
-
-  return false;
+function isNegative(property, idx) {
+  return property[idx] && property[idx][0][0] == '-' && parseFloat(property[idx][0]) < 0;
 }
 
-function zeroMinifier(_, value) {
+function zeroMinifier(name, value) {
   if (value.indexOf('0') == -1)
     return value;
 
@@ -110,6 +73,21 @@ function zeroDegMinifier(_, value) {
   return value.replace(/\(0deg\)/g, '(0)');
 }
 
+function whitespaceMinifier(name, value) {
+  if (name.indexOf('filter') > -1 || value.indexOf(' ') == -1)
+    return value;
+
+  value = value.replace(/\s+/g, ' ');
+
+  if (value.indexOf('calc') > -1)
+    value = value.replace(/\) ?\/ ?/g, ')/ ');
+
+  return value
+    .replace(/\( /g, '(')
+    .replace(/ \)/g, ')')
+    .replace(/, /g, ',');
+}
+
 function precisionMinifier(_, value, precisionOptions) {
   if (precisionOptions.value === -1 || value.indexOf('.') === -1)
     return value;
@@ -125,14 +103,13 @@ function unitMinifier(_, value, unitsRegexp) {
   return value.replace(unitsRegexp, '$1' + '0');
 }
 
-function multipleZerosMinifier(name, value) {
-  if (value.indexOf('0 0 0 0') == -1)
-    return value;
-
-  if (name.indexOf('box-shadow') > -1)
-    return value == '0 0 0 0' ? '0 0' : value;
-
-  return value.replace(/^0 0 0 0$/, '0');
+function multipleZerosMinifier(property) {
+  if (property.length == 5 && property[1][0] === '0' && property[2][0] === '0' && property[3][0] === '0' && property[4][0] === '0') {
+    if (property[0][0].indexOf('box-shadow') > -1)
+      property.splice(3);
+    else
+      property.splice(2);
+  }
 }
 
 function colorMininifier(_, value, compatibility) {
@@ -177,60 +154,89 @@ function colorMininifier(_, value, compatibility) {
   return HexNameShortener.shorten(value);
 }
 
-function spaceMinifier(name, value) {
-  if (name == 'filter' || value.indexOf(') ') == -1 || processable.implementedFor.test(name))
-    return value;
+function minifyBorderRadius(property) {
+  if (property.length == 4 && property[2][0] == '/' && property[1][0] == property[3][0])
+    property.splice(2);
+  else if (property.length == 6 && property[3][0] == '/' && property[1][0] == property[4][0] && property[2][0] == property[5][0])
+    property.splice(3);
+  else if (property.length == 8 && property[4][0] == '/' && property[1][0] == property[5][0] && property[2][0] == property[6][0] && property[3][0] == property[7][0])
+    property.splice(4);
+  else if (property.length == 10 && property[5][0] == '/' && property[1][0] == property[6][0] && property[2][0] == property[7][0] && property[3][0] == property[8][0] && property[4][0] == property[9][0])
+    property.splice(5);
+}
+
+function minifyFilter(property) {
+  if (property.length < 3) {
+    property[1][0] = property[1][0].replace(/progid:DXImageTransform\.Microsoft\.(Alpha|Chroma)(\W)/, function (match, filter, suffix) {
+      return filter.toLowerCase() + suffix;
+    });
+  }
 
-  return value.replace(/\) ((?![\+\-] )|$)/g, ')$1');
+  property[1][0] = property[1][0]
+    .replace(/,(\S)/g, ', $1')
+    .replace(/ ?= ?/g, '=');
+}
+
+function minifyFont(property) {
+  if (property[2] && property[2][0] != 'normal' && property[2][0] != 'bold' && !/^[1-9]00/.test(property[2][0]))
+    property[1][0] = valueMinifiers['font-weight'](property[1][0]);
 }
 
 function optimizeBody(properties, options) {
-  var property, firstColon, name, value, important;
+  var property, name, value, unused;
 
   for (var i = 0, l = properties.length; i < l; i++) {
+    unused = false;
     property = properties[i];
 
     // FIXME: the check should be gone with #407
-    if (property[0].indexOf('__ESCAPED_') === 0)
+    if (typeof property == 'string' && property.indexOf('__ESCAPED_') === 0)
       continue;
 
-    firstColon = property[0].indexOf(':');
-    name = property[0].substring(0, firstColon);
-    value = property[0].substring(firstColon + 1);
-    important = false;
+    name = property[0][0];
+
+    if (property[0][2]) {
+      var isPrefixHack = name[0] == '_';
+      if (isPrefixHack && !options.compatibility.properties.iePrefixHack || !isPrefixHack && !options.compatibility.properties.ieSuffixHack)
+        unused = true;
+    }
+
+    if (name.indexOf('padding') === 0 && (isNegative(property, 1) || isNegative(property, 2) || isNegative(property, 3) || isNegative(property, 4)))
+      unused = true;
 
-    if ((!options.compatibility.properties.iePrefixHack && (name[0] == '_' || name[0] == '*')) ||
-        (name.indexOf('padding') === 0 && isNegative(value))) {
+    if (unused) {
       properties.splice(i, 1);
       i--;
       l--;
       continue;
     }
 
-    if (value.indexOf('!important') > 0 || value.indexOf('! important') > 0) {
-      value = value.substring(0, value.indexOf('!')).trim();
-      important = true;
-    }
+    for (var j = 1, m = property.length; j < m; j++) {
+      value = property[j][0];
 
-    if (name.indexOf('border') === 0 && name.indexOf('radius') > 0)
-      value = valueMinifiers['border-*-radius'](value);
+      if (valueMinifiers[name])
+        value = valueMinifiers[name](value);
 
-    if (valueMinifiers[name])
-      value = valueMinifiers[name](value);
+      value = whitespaceMinifier(name, value);
+      value = precisionMinifier(name, value, options.precision);
+      value = zeroMinifier(name, value);
+      if (options.compatibility.properties.zeroUnits) {
+        value = zeroDegMinifier(name, value);
+        value = unitMinifier(name, value, options.unitsRegexp);
+      }
+      value = colorMininifier(name, value, options.compatibility);
 
-    value = precisionMinifier(name, value, options.precision);
-    value = zeroMinifier(name, value);
-    if (options.compatibility.properties.zeroUnits) {
-      value = zeroDegMinifier(name, value);
-      value = unitMinifier(name, value, options.unitsRegexp);
+      property[j][0] = value;
     }
-    value = multipleZerosMinifier(name, value);
-    value = colorMininifier(name, value, options.compatibility);
 
-    if (!options.compatibility.properties.spaceAfterClosingBrace)
-      value = spaceMinifier(name, value);
+    multipleZerosMinifier(property);
 
-    property[0] = name + ':' + value + (important ? '!important' : '');
+    if (name.indexOf('border') === 0 && name.indexOf('radius') > 0)
+      minifyBorderRadius(property);
+    else if (name == 'filter')
+      minifyFilter(property);
+    else if (name == 'font')
+      minifyFont(property);
   }
 }
 
similarity index 88%
rename from lib/properties/reorderable.js
rename to lib/selectors/reorderable.js
index 902d690..50b39c5 100644 (file)
@@ -1,3 +1,5 @@
+// TODO: it'd be great to merge it with the other canReorder functionality
+
 var FLEX_PROPERTIES = /align\-items|box\-align|box\-pack|flex|justify/;
 
 function canReorder(left, right) {
@@ -15,13 +17,13 @@ function canReorderSingle(left, right) {
   var leftName = left[0];
   var leftValue = left[1];
   var leftNameRoot = left[2];
-  var leftSelector = left[4];
-  var leftInSimpleSelector = left[5];
+  var leftSelector = left[5];
+  var leftInSimpleSelector = left[6];
   var rightName = right[0];
   var rightValue = right[1];
   var rightNameRoot = right[2];
-  var rightSelector = right[4];
-  var rightInSimpleSelector = right[5];
+  var rightSelector = right[5];
+  var rightInSimpleSelector = right[6];
 
   if (leftName == 'font' && rightName == 'line-height' || rightName == 'font' && leftName == 'line-height')
     return false;
index f744fda..5851919 100644 (file)
@@ -3,6 +3,15 @@ var SourceMapGenerator = require('source-map').SourceMapGenerator;
 var lineBreak = require('os').EOL;
 var unknownSource = '$stdin';
 
+function hasMoreProperties(elements, index) {
+  for (var i = index, l = elements.length; i < l; i++) {
+    if (typeof elements[i] != 'string')
+      return true;
+  }
+
+  return false;
+}
+
 function Rebuilder(options, restoreCallback, inputMapTracker) {
   this.column = 0;
   this.line = 1;
@@ -14,27 +23,47 @@ function Rebuilder(options, restoreCallback, inputMapTracker) {
   this.outputMap = new SourceMapGenerator();
 }
 
-Rebuilder.prototype.rebuildValue = function (elements, separator) {
-  var escaped = 0;
-
+Rebuilder.prototype.rebuildSelectors = function (elements) {
   for (var i = 0, l = elements.length; i < l; i++) {
     var element = elements[i];
+    this.store(element);
+
+    if (i < l - 1)
+      this.store(',');
+  }
+};
 
-    if (element[0].indexOf('__ESCAPED_') === 0) {
-      this.store(element[0]);
-      escaped++;
+Rebuilder.prototype.rebuildBody = function (elements) {
+  for (var i = 0, l = elements.length; i < l; i++) {
+    var element = elements[i];
 
-      if (i === l - 1 && escaped > 0)
-        this.output.splice(this.output.length - escaped - 1, 1);
-    } else {
+    if (typeof element == 'string') {
       this.store(element);
-      this.store(i < l - 1 ? separator : '');
-      escaped = 0;
+      continue;
+    }
+
+    for (var j = 0, m = element.length; j < m; j++) {
+      this.store(element[j]);
+
+      if (j == m - 1 && element[0][1])
+        this.store('!important');
+
+      if (j === 0) {
+        this.store(':');
+      } else if (j < m - 1) {
+        this.store(' ');
+      } else if (i < l - 1 && hasMoreProperties(elements, i + 1)) {
+        this.store(';');
+      }
     }
   }
 };
 
 Rebuilder.prototype.store = function (element) {
+  // handles defaults and values like `,` or `/` which do not have mapping
+  if (Array.isArray(element) && element.length == 1)
+    element = element[0];
+
   var fromString = typeof element == 'string';
   var value = fromString ? element : element[0];
 
@@ -57,23 +86,23 @@ Rebuilder.prototype.rebuildList = function (tokens, isFlatBlock) {
         this.store(token[1][0]);
         break;
       case 'block':
-        this.rebuildValue([token[1]], '');
+        this.rebuildSelectors([token[1]]);
         this.store('{');
         this.rebuildList(token[2], false);
         this.store('}');
         this.store(joinCharacter);
         break;
       case 'flat-block':
-        this.rebuildValue([token[1]], '');
+        this.rebuildSelectors([token[1]]);
         this.store('{');
-        this.rebuildValue(token[2], ';');
+        this.rebuildBody(token[2]);
         this.store('}');
         this.store(joinCharacter);
         break;
       default:
-        this.rebuildValue(token[1], ',');
+        this.rebuildSelectors(token[1]);
         this.store('{');
-        this.rebuildValue(token[2], ';');
+        this.rebuildBody(token[2]);
         this.store('}');
         this.store(joinCharacter);
     }
@@ -90,7 +119,11 @@ Rebuilder.prototype.track = function (value, element) {
 };
 
 Rebuilder.prototype.trackMetadata = function (element) {
-  var source = element[3] || unknownSource;
+  var sourceAt = element.length - 1;
+  if (typeof element[sourceAt] == 'object')
+    sourceAt--;
+
+  var source = element[sourceAt] || unknownSource;
 
   this.outputMap.addMapping({
     generated: {
@@ -99,13 +132,13 @@ Rebuilder.prototype.trackMetadata = function (element) {
     },
     source: source,
     original: {
-      line: element[1],
-      column: element[2]
+      line: element[sourceAt - 2],
+      column: element[sourceAt - 1]
     }
   });
 
-  if (element[4])
-    this.outputMap.setSourceContent(source, element[4][element[3]]);
+  if (element[sourceAt + 1])
+    this.outputMap.setSourceContent(source, element[sourceAt + 1][element[sourceAt]]);
 };
 
 function SourceMapStringifier(options, restoreCallback, inputMapTracker) {
index 430fdaf..ce57f93 100644 (file)
@@ -5,22 +5,53 @@ function Stringifier(options, restoreCallback) {
   this.restoreCallback = restoreCallback;
 }
 
-function valueRebuilder(elements, separator) {
+function selectorRebuilder(elements) {
   var merged = '';
-  var element;
+
+  for (var i = 0, l = elements.length; i < l; i++) {
+    merged += elements[i] + (i < l - 1 ? ',' : '');
+  }
+
+  return merged;
+}
+
+function bodyRebuilder(elements) {
+  var merged = '';
+  var element, important, lastSemicolonAt, value, valueLastChar, shouldSkipSpaceAfter;
 
   for (var i = 0, l = elements.length; i < l; i++) {
     element = elements[i];
 
-    if (element[0].indexOf('__ESCAPED_') === 0) {
-      merged += element[0];
+    if (typeof element == 'string' && element.indexOf('__ESCAPED_') === 0) {
+      merged += element;
 
       if (i === l - 1) {
-        var lastSemicolonAt = merged.lastIndexOf(';');
+        lastSemicolonAt = merged.lastIndexOf(';');
         merged = merged.substring(0, lastSemicolonAt) + merged.substring(lastSemicolonAt + 1);
       }
     } else {
-      merged += element[0] + (i < l - 1 ? separator : '');
+      important = element[0][1];
+      merged += element[0][0] + ':';
+
+      for (var j = 1, m = element.length; j < m; j++) {
+        value = element[j][0];
+        valueLastChar = value[value.length - 1];
+
+        if (value == ',' || value == '/') {
+          if (merged[merged.length - 1] == ')')
+            merged += value;
+          else
+            merged = merged.substring(0, merged.length - 1) + value;
+        } else {
+          shouldSkipSpaceAfter = j == m - 1 || valueLastChar == ')' && value.indexOf('progid') == -1;
+          merged += value + (shouldSkipSpaceAfter ? '' : ' ');
+        }
+      }
+
+      if (important)
+        merged += '!important';
+
+      merged += (i < l - 1 ? ';' : '');
     }
   }
 
@@ -47,13 +78,13 @@ function rebuild(tokens, keepBreaks, isFlatBlock) {
           parts.push(token[1][0] + '{' + body + '}');
         break;
       case 'flat-block':
-        body = valueRebuilder(token[2], ';');
+        body = bodyRebuilder(token[2]);
         if (body.length > 0)
           parts.push(token[1][0] + '{' + body + '}');
         break;
       default:
-        selector = valueRebuilder(token[1], ',');
-        body = valueRebuilder(token[2], ';');
+        selector = selectorRebuilder(token[1]);
+        body = bodyRebuilder(token[2]);
         parts.push(selector + '{' + body + '}');
     }
   }
index 8f9867b..1ad6d08 100644 (file)
 var Splitter = require('./splitter');
 
+var COMMA = ',';
+var FORWARD_SLASH = '/';
+
 var Extractors = {
   properties: function (string, context) {
     var list = [];
-    var buffer = [];
-    var all = [];
-    var property;
-    var isPropertyEnd;
-    var isWhitespace;
-    var wasWhitespace;
-    var isSpecial;
-    var wasSpecial;
-    var current;
-    var last;
-    var secondToLast;
-    var wasCloseParenthesis;
-    var isEscape;
-    var metadata;
+    var splitter = new Splitter(/[ ,\/]/);
 
-    if (string.replace && string.indexOf(')') > 0)
-      string = string.replace(/\)([^\s_;:,\)])/g, context.sourceMaps ? ') __ESCAPED_COMMENT_CLEAN_CSS(0,-1)__$1' : ') $1');
-
-    for (var i = 0, l = string.length; i < l; i++) {
-      current = string[i];
-      isPropertyEnd = current === ';';
-
-      isEscape = !isPropertyEnd && current == '_' && string.indexOf('__ESCAPED_COMMENT', i) === i;
-      if (isEscape) {
-        if (buffer.length > 0) {
-          i--;
-          isPropertyEnd = true;
-        } else {
-          var endOfEscape = string.indexOf('__', i + 1) + 2;
-          var comment = string.substring(i, endOfEscape);
-          i = endOfEscape - 1;
-
-          if (comment.indexOf('__ESCAPED_COMMENT_SPECIAL') === -1) {
-            context.track(comment);
-            continue;
-          }
-          else {
-            buffer = all = [comment];
-          }
-        }
+    if (typeof string != 'string')
+      return [];
+
+    if (string.indexOf('__ESCAPED_COMMENT_') > -1)
+      string = string.replace(/(__ESCAPED_COMMENT_(SPECIAL_)?CLEAN_CSS[^_]+?__)/g, ';$1;');
+
+    if (string.indexOf(')') > -1)
+      string = string.replace(/\)([^\s_;:,\)])/g, context.sourceMaps ? ') __ESCAPED_COMMENT_CLEAN_CSS(0,-1)__ $1' : ') $1');
+
+    if (string.indexOf('ESCAPED_URL_CLEAN_CSS') > -1)
+      string = string.replace(/(ESCAPED_URL_CLEAN_CSS[^_]+?__)/g, context.sourceMaps ? '$1 __ESCAPED_COMMENT_CLEAN_CSS(0,-1)__ ' : '$1 ');
+
+    var candidates = string.split(';');
+
+    for (var i = 0, l = candidates.length; i < l; i++) {
+      var candidate = candidates[i];
+      var firstColonAt = candidate.indexOf(':');
+
+      if (firstColonAt == -1) {
+        context.track(candidate);
+        if (candidate.indexOf('__ESCAPED_COMMENT_SPECIAL') > -1)
+          list.push(candidate);
+        continue;
       }
 
-      if (isPropertyEnd || isEscape) {
-        if (wasWhitespace && buffer[buffer.length - 1] === ' ')
-          buffer.pop();
-        if (buffer.length > 0) {
-          property = buffer.join('');
-          if (property.indexOf('{') === -1) {
-            metadata = context.track(all.join(''), true);
-            list.push([property].concat(metadata));
-
-            if (!isEscape)
-              context.track(';');
-          }
+      if (candidate.indexOf('{') > 0) {
+        context.track(candidate);
+        continue;
+      }
+
+      var body = [];
+      var name = candidate.substring(0, firstColonAt);
+      body.push([name.trim()].concat(context.track(name, true)));
+      context.track(':');
+
+      var values = splitter.split(candidate.substring(firstColonAt + 1), true);
+
+      for (var j = 0, m = values.length; j < m; j++) {
+        var value = values[j];
+        var trimmed = value.trim();
+
+        if (trimmed.length === 0)
+          continue;
+
+        var lastCharacter = trimmed[trimmed.length - 1];
+        var endsWithNonSpaceSeparator = trimmed.length > 1 && (lastCharacter == COMMA || lastCharacter == FORWARD_SLASH);
+
+        if (endsWithNonSpaceSeparator)
+          trimmed = trimmed.substring(0, trimmed.length - 1);
+
+        if (trimmed.indexOf('__ESCAPED_COMMENT_CLEAN_CSS(0,-') > -1) {
+          context.track(trimmed);
+          continue;
+        }
+
+        var pos = body.length - 1;
+        if (trimmed == 'important' && body[pos][0] == '!') {
+          context.track(trimmed);
+          body[pos - 1][0] += '!important';
+          body.pop();
+          continue;
         }
-        buffer = [];
-        all = [];
-      } else {
-        isWhitespace = current === ' ' || current === '\t' || current === '\n';
-        isSpecial = current === ':' || current === '[' || current === ']' || current === ',' || current === '(' || current === ')';
-
-        if (wasWhitespace && isSpecial) {
-          last = buffer[buffer.length - 1];
-          secondToLast = buffer[buffer.length - 2];
-          if (secondToLast != '+' && secondToLast != '-' && secondToLast != '/' && secondToLast != '*' && last != '(')
-            buffer.pop();
-          buffer.push(current);
-        } else if (isWhitespace && wasSpecial && !wasCloseParenthesis) {
-        } else if (isWhitespace && !wasWhitespace && buffer.length > 0) {
-          buffer.push(' ');
-        } else if (isWhitespace && buffer.length === 0) {
-        } else if (isWhitespace && wasWhitespace) {
-        } else {
-          buffer.push(isWhitespace ? ' ' : current);
+
+        if (trimmed == '!important' || (trimmed == 'important' && body[pos][0][body[pos][0].length - 1] == '!')) {
+          context.track(trimmed);
+          body[pos][0] += trimmed;
+          continue;
         }
 
-        all.push(current);
+        body.push([trimmed].concat(context.track(value, true)));
+
+        if (endsWithNonSpaceSeparator) {
+          body.push([lastCharacter]);
+          context.track(lastCharacter);
+        }
       }
 
-      wasSpecial = isSpecial;
-      wasWhitespace = isWhitespace;
-      wasCloseParenthesis = current === ')';
-    }
+      if (i < l - 1)
+        context.track(';');
 
-    if (wasWhitespace && buffer[buffer.length - 1] === ' ')
-      buffer.pop();
-    if (buffer.length > 0) {
-      property = buffer.join('');
-      if (property.indexOf('{') === -1) {
-        metadata = context.track(all.join(''), true);
-        list.push([property].concat(metadata));
-      }
-    } else if (all.indexOf('\n') > -1) {
-      context.track(all.join(''));
+      list.push(body);
     }
 
     return list;
@@ -110,7 +102,7 @@ var Extractors = {
     for (var i = 0, l = selectors.length; i < l; i++) {
       metadata = context.track(selectors[i], true, i);
       context.track(',');
-      list.push([selectors[i]].concat(metadata));
+      list.push([selectors[i].trim()].concat(metadata));
     }
 
     return list;
index 2d3bb47..a856070 100644 (file)
@@ -1,9 +1,13 @@
 function Splitter(separator) {
   this.separator = separator;
+  this.withRegex = typeof separator != 'string';
 }
 
 Splitter.prototype.split = function (value, withSeparator) {
-  if (value.indexOf(this.separator) === -1)
+  var hasSeparator = this.withRegex ?
+    this.separator.test(value) :
+    value.indexOf(this.separator);
+  if (!hasSeparator)
     return [value];
 
   if (value.indexOf('(') === -1 && !withSeparator)
@@ -20,7 +24,7 @@ Splitter.prototype.split = function (value, withSeparator) {
       level++;
     } else if (value[cursor] == ')') {
       level--;
-    } else if (value[cursor] == this.separator && level === 0) {
+    } else if ((this.withRegex ? this.separator.test(value[cursor]) : value[cursor] == this.separator) && level === 0) {
       tokens.push(value.substring(lastStart, cursor + (withSeparator ? 1 : 0)));
       lastStart = cursor + 1;
     }
index 0caf9f1..b77a0b0 100644 (file)
@@ -1,24 +1,39 @@
-function stringify(values, separator) {
-  var i = 0;
-  var result = [];
-
-  while (values[i]) {
-    result.push(values[i][0]);
-    i++;
+function stringifyValue(property) {
+  var result = '';
+  for (var i = 1, l = property.length; i < l; i++) {
+    result += property[i][0] + (i < l - 1 ? ' ' : '');
   }
 
-  return result.join(separator);
+  return result;
 }
 
 function stringifyBody(properties) {
-  return stringify(properties, ';');
+  var result = '';
+  for (var i = 0, l = properties.length; i < l; i++) {
+    var property = properties[i];
+
+    result += property[0][0] + ':';
+    for (var j = 1, m = property.length; j < m; j++) {
+      result += property[j][0] + (j < m - 1 ? ' ' : '');
+    }
+
+    result += (i < l - 1 ? ';' : '');
+  }
+
+  return result;
 }
 
 function stringifySelector(list) {
-  return stringify(list, ',');
+  var result = '';
+  for (var i = 0, l = list.length; i < l; i++) {
+    result += list[i][0] + (i < l - 1 ? ',' : '');
+  }
+
+  return result;
 }
 
 module.exports = {
+  value: stringifyValue,
   body: stringifyBody,
   selector: stringifySelector
 };
index 4f58e61..ebbca68 100644 (file)
@@ -332,7 +332,7 @@ exports.commandsSuite = vows.describe('binary commands').addBatch({
     }),
     'of mergeable properties': pipedContext('a{background:red;display:block;background:white}', '--skip-aggressive-merging', {
       'gets right result': function(error, stdout) {
-        assert.equal(stdout, 'a{display:block;background:#fff}');
+        assert.equal(stdout, 'a{background:#fff;display:block}');
       }
     })
   },
@@ -475,7 +475,7 @@ exports.commandsSuite = vows.describe('binary commands').addBatch({
         var count = 0;
         sourceMap.eachMapping(function () { count++; });
 
-        assert.equal(count, 4);
+        assert.equal(count, 6);
       },
       'teardown': function () {
         deleteFile('import.min.css');
@@ -491,7 +491,7 @@ exports.commandsSuite = vows.describe('binary commands').addBatch({
         var count = 0;
         sourceMap.eachMapping(function () { count++; });
 
-        assert.equal(count, 4);
+        assert.equal(count, 6);
       },
       'teardown': function () {
         deleteFile('import-inline.min.css');
index 54371da..11bf567 100644 (file)
@@ -30,33 +30,6 @@ var cssContext = function(groups, options) {
   return context;
 };
 
-var redefineContext = function(redefinitions, options) {
-  var context = {};
-  var vendorPrefixes = ['', '-moz-', '-o-', '-webkit-']; // there is no -ms-animation nor -ms-transition.
-
-  for (var property in redefinitions) {
-    for (var i = 0; i < redefinitions[property].length; i++) {
-      var by = redefinitions[property][i];
-      var prefixes = options.vendorPrefixes.indexOf(by) > -1 ? vendorPrefixes : [''];
-
-      for (var j = 0, m = prefixes.length; j < m; j++) {
-        var prefixedProperty = prefixes[j] + property;
-        var prefixedBy = prefixes[j] + by;
-        var zeroValue = options.noneFor.indexOf(prefixedProperty) > -1 ? 'none' : '0';
-
-        context['should override ' + prefixedProperty + ' by ' + prefixedBy] = [
-          'a{' + prefixedProperty + ':inherit;' + prefixedBy + ':' + zeroValue + '}',
-          'a{' + prefixedBy + ':' + zeroValue + '}'
-        ];
-        context['should not override ' + prefixedBy + ' by ' + prefixedProperty] =
-          'a{' + prefixedBy + ':' + zeroValue + ';' + prefixedProperty + ':inherit}';
-      }
-    }
-  }
-
-  return cssContext(context);
-};
-
 vows.describe('integration tests').addBatch({
   'identity': cssContext({
     'preserve minified content': 'a{color:#f10}'
@@ -126,7 +99,7 @@ vows.describe('integration tests').addBatch({
     ],
     'not inside calc method with more parentheses': [
       'div{height:-moz-calc((10% + 12px)/2 + 10em)}',
-      'div{height:-moz-calc((10% + 12px)/2 + 10em)}'
+      'div{height:-moz-calc((10% + 12px)/ 2 + 10em)}'
     ],
     'not inside calc method with multiplication': [
       'div{height:-moz-calc(3 * 2em + 10px)}',
@@ -1684,63 +1657,11 @@ title']{display:block}",
       '@media (min-width:100px){a{color:red}}@media screen{p{width:100px}a{color:red}}'
     ]
   }),
-  'duplicate properties': cssContext({
-    'of two properties one after another': 'a{display:-moz-inline-box;display:inline-block}',
-    'of two properties in one declaration': [
-      'a{display:inline-block;color:red;display:block}',
-      'a{color:red;display:block}'
-    ],
-    'of two properties in one declaration with former as !important': [
-      'a{display:inline-block!important;color:red;display:block}',
-      'a{display:inline-block!important;color:red}'
-    ],
-    'of two properties in one declaration with latter as !important': [
-      'a{display:inline-block;color:red;display:block!important}',
-      'a{color:red;display:block!important}'
-    ],
-    'of two properties in one declaration with both as !important': [
-      'a{display:inline-block!important;color:red;display:block!important}',
-      'a{color:red;display:block!important}'
-    ],
-    'of two properties in one declaration with both as !important but 2nd less understandable': 'a{color:red!important;display:block;color:rgba(0,255,0,.5)!important}',
-    'of two properties in one declaration with both as !important but 2nd more understandable': 'a{color:rgba(0,255,0,.5)!important;display:block;color:red!important}',
-    'of two shorthand properties in one declaration with both as !important but 2nd less understandable': 'a{background:red!important;background:rgba(0,255,0,.5)!important}',
-    'of two shorthand properties in one declaration with both as !important but 2nd more understandable': 'a{background:rgba(0,255,0,.5)!important;background:red!important}',
-    'of many properties in one declaration': [
-      'a{display:inline-block;color:red;font-weight:bolder;font-weight:700;display:block!important;color:#fff}',
-      'a{font-weight:bolder;font-weight:700;display:block!important;color:#fff}'
-    ],
-    'both redefined and overridden': [
-      'p{display:block;display:-moz-inline-box;color:red;display:table-cell}',
-      'p{color:red;display:table-cell}'
-    ],
-    'background redefined with merging': [
-      '.one{display:block}.one{background:#fff;background:-webkit-gradient();background:-moz-linear-gradient();filter:progid:DXImageTransform}',
-      '.one{display:block;background:#fff;background:-webkit-gradient();background:-moz-linear-gradient();filter:progid:DXImageTransform}'
-    ],
-    'filter treated as background': 'p{background:-moz-linear-gradient();background:-webkit-linear-gradient();filter:"progid:DXImageTransform";background:linear-gradient()}',
-    'filter treated as background-image': 'p{background-image:-moz-linear-gradient();background-image:-webkit-linear-gradient();filter:"progid:DXImageTransform";background-image:linear-gradient()}',
-    '-ms-filter treated as background': 'p{background:-moz-linear-gradient();background:-webkit-linear-gradient();-ms-filter:"progid:DXImageTransform";background:linear-gradient()}',
-    '-ms-filter treated as background-image': 'p{background-image:-moz-linear-gradient();background-image:-webkit-linear-gradient();-ms-filter:"progid:DXImageTransform";background-image:linear-gradient()}',
-    '-ms-transform with different values #1': 'div{-ms-transform:translate(0,0);-ms-transform:translate3d(0,0,0)}',
-    '-ms-transform with different values #2': 'div{-ms-transform:translate(0,0);-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0)}',
-    'transform with different values #1': 'div{transform:translate(0,0);transform:translate3d(0,0,0)}',
-    'transform with different values #2': 'div{transform:translate(0,0);-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}',
-    'border(hex) with border(rgba)': 'a{border:1px solid #fff;display:none;border:1px solid rgba(1,0,0,.5)}',
-    'border(hex !important) with border(hex)': [
-      'a{border:1px solid #fff!important;display:none;border:1px solid #fff}',
-      'a{border:1px solid #fff!important;display:none}'
-    ],
-    'border(hex) with border(hex !important)': [
-      'a{border:1px solid #fff;display:none;border:1px solid #fff!important}',
-      'a{display:none;border:1px solid #fff!important}'
-    ]
-  }),
   'duplicate properties with aggressive merging disabled': cssContext({
     'of (yet) unmergeable properties': 'a{display:inline-block;color:red;display:-moz-block}',
     'of mergeable properties': [
       'a{background:red;display:block;background:white}',
-      'a{display:block;background:#fff}'
+      'a{background:#fff;display:block}'
     ]
   }, { aggressiveMerging: false }),
   'same selectors': cssContext({
@@ -1940,66 +1861,6 @@ title']{display:block}",
   'units - IE8 compatibility': cssContext({
     'rems': 'div{padding-top:16px;padding-top:1rem}'
   }, { compatibility: 'ie8' }),
-  'redefined more granular properties': redefineContext({
-    'animation-delay': ['animation'],
-    'animation-direction': ['animation'],
-    'animation-duration': ['animation'],
-    'animation-fill-mode': ['animation'],
-    'animation-iteration-count': ['animation'],
-    'animation-name': ['animation'],
-    'animation-play-state': ['animation'],
-    'animation-timing-function': ['animation'],
-    'background-attachment': ['background'],
-    'background-clip': ['background'],
-    'background-color': ['background'],
-    'background-image': ['background'],
-    'background-origin': ['background'],
-    'background-position': ['background'],
-    'background-repeat': ['background'],
-    'background-size': ['background'],
-    'border-color': ['border'],
-    'border-style': ['border'],
-    'border-width': ['border'],
-    'border-bottom': ['border'],
-    'border-bottom-color': ['border-bottom', 'border-color', 'border'],
-    'border-bottom-style': ['border-bottom', 'border-style', 'border'],
-    'border-bottom-width': ['border-bottom', 'border-width', 'border'],
-    'border-left': ['border'],
-    'border-left-color': ['border-left', 'border-color', 'border'],
-    'border-left-style': ['border-left', 'border-style', 'border'],
-    'border-left-width': ['border-left', 'border-width', 'border'],
-    'border-right': ['border'],
-    'border-right-color': ['border-right', 'border-color', 'border'],
-    'border-right-style': ['border-right', 'border-style', 'border'],
-    'border-right-width': ['border-right', 'border-width', 'border'],
-    'border-top': ['border'],
-    'border-top-color': ['border-top', 'border-color', 'border'],
-    'border-top-style': ['border-top', 'border-style', 'border'],
-    'border-top-width': ['border-top', 'border-width', 'border'],
-    'font-family': ['font'],
-    'font-size': ['font'],
-    'font-style': ['font'],
-    'font-variant': ['font'],
-    'font-weight': ['font'],
-    'list-style-image': ['list-style'],
-    'list-style-position': ['list-style'],
-    'list-style-type': ['list-style'],
-    'margin-bottom': ['margin'],
-    'margin-left': ['margin'],
-    'margin-right': ['margin'],
-    'margin-top': ['margin'],
-    'outline-color': ['outline'],
-    'outline-style': ['outline'],
-    'outline-width': ['outline'],
-    'padding-bottom': ['padding'],
-    'padding-left': ['padding'],
-    'padding-right': ['padding'],
-    'padding-top': ['padding'],
-    'transition-delay': ['transition'],
-    'transition-duration': ['transition'],
-    'transition-property': ['transition'],
-    'transition-timing-function': ['transition']
-  }, { vendorPrefixes: ['animation', 'transition'], noneFor: ['list-style-image'] }),
   'redefined more granular properties with property merging': cssContext({
     'should merge background with background-attachment': [
       'a{background:0;background-attachment:fixed}',
@@ -2018,8 +1879,8 @@ title']{display:block}",
       'a{background:0;background-color:inherit}'
     ],
     'should NOT merge background with background-color set to none': [
-      'a{background:url(logo.png)no-repeat center;background-color:none}',
-      'a{background:url(logo.png)no-repeat center;background-color:none}'
+      'a{background:url(logo.png)center no-repeat;background-color:none}',
+      'a{background:url(logo.png)center no-repeat;background-color:none}'
     ],
     'should merge background with background-image': [
       'a{background:0;background-image:url(hello_world)}',
@@ -2074,210 +1935,6 @@ title']{display:block}",
       'li{list-style:inside}'
     ]
   }),
-  'shorthand properties': cssContext({
-    'shorthand background #1' : [
-      'div{background-color:#111;background-image:url(aaa);background-repeat:repeat;background-position:0 0;background-attachment:scroll;background-size:auto;background-origin:padding-box;background-clip:border-box}',
-      'div{background:url(aaa)#111}'
-    ],
-    'shorthand background #2' : [
-      'div{background-color:#111;background-image:url(aaa);background-repeat:no-repeat;background-position:0 0;background-attachment:scroll;background-size:auto;background-origin:padding-box;background-clip:border-box}',
-      'div{background:url(aaa)no-repeat #111}'
-    ],
-    '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;background-size:auto!important;background-origin:padding-box!important;background-clip:border-box!important}',
-      'div{background:url(aaa)#111!important}'
-    ],
-    'shorthand important background overriding': [
-      'a{background:url(a.jpg) !important; background-color:#fff !important}',
-      'a{background:url(a.jpg)#fff!important}'
-    ],
-    'shorthand important background overriding by non-mergeable property': [
-      'a{background:url(a.jpg) !important; background-color:#fff !important; background-size:10px 10px !important}',
-      'a{background:url(a.jpg)#fff!important;background-size:10px 10px!important}'
-    ],
-    'shorthand background-repeat correctly': [
-      'a{background:url(/image/path.png) no-repeat repeat}',
-      'a{background:url(/image/path.png)no-repeat repeat}'
-    ],
-    'shorthand border-width': [
-      '.t{border-top-width:7px;border-bottom-width:7px;border-left-width:4px;border-right-width:4px}',
-      '.t{border-width:7px 4px}'
-    ],
-    'shorthand border-color #1': [
-      '.t{border-top-color:#9fce00;border-bottom-color:#9fce00;border-left-color:#9fce00;border-right-color:#9fce00}',
-      '.t{border-color:#9fce00}'
-    ],
-    'shorthand border-color #2': [
-      '.t{border-right-color:#002;border-bottom-color:#003;border-top-color:#001;border-left-color:#004}',
-      '.t{border-color:#001 #002 #003 #004}'
-    ],
-    'shorthand border-radius': [
-      '.t{border-top-left-radius:7px;border-bottom-right-radius:6px;border-bottom-left-radius:5px;border-top-right-radius:3px}',
-      '.t{border-radius:7px 3px 6px 5px}'
-    ],
-    'shorthand border-radius none': 'li{border-radius:none}',
-    'shorthand list-style #1': [
-      '.t{list-style-type:circle;list-style-position:outside;list-style-image:url(aaa)}',
-      '.t{list-style:circle url(aaa)}'
-    ],
-    'shorthand list-style #2': [
-      '.t{list-style-image:url(aaa);list-style-type:circle;list-style-position:inside}',
-      '.t{list-style:circle inside url(aaa)}'
-    ]
-  }),
-  'cares 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:linear-gradient(whatever) #222}',
-      'div{background:#fff;background:linear-gradient(whatever)#222}'
-    ],
-    '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;background-size:auto;background-origin:padding-box;background-clip:border-box}',
-      'div{background:#111;background:linear-gradient(aaa)#111}'
-    ]
-  }),
-  'cares about understandability of border components': cssContext({
-    'border(none) with border(rgba)': 'a{border:none;border:1px solid rgba(1,0,0,.5)}',
-    'border(rgba) with border(none)': 'a{border:1px solid rgba(1,0,0,.5);border:none}',
-    'border(hex) with border(rgba)': 'a{border:1px solid #fff;border:1px solid rgba(1,0,0,.5)}'
-  }),
-  '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;background-size:auto;background-origin:padding-box;background-clip:border-box}',
-      'p{background-color:#9fce00!important;background:url(hello)1px 2px repeat-y}'
-    ],
-    '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:url(hello)#9fce00;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;;background-size:inherit;background-origin:inherit;background-clip: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;background-size:inherit;background-origin:inherit;background-clip:inherit}',
-      '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;background-size:auto;background-clip:inherit;background-origin:padding-box;}',
-      'p{background:1px 2px repeat-y #9fce00;background-image:inherit;background-clip: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}'
-    ],
-    'default background components should be removed #3': [
-      'body{background:none repeat scroll 0 0 #000}',
-      'body{background:#000}'
-    ]
-  }),
   'merging of rules': cssContext({
     'rules without pseudo classes should be merged': [
       'a{color:red}b{color:red}',
@@ -2324,30 +1981,6 @@ title']{display:block}",
       'a:first{color:red}b{color:red}'
     ]
   }),
-  'complex granular properties': cssContext({
-    'two granular properties': 'a{border-bottom:1px solid red;border-color:red}',
-    'more understandable granular property should override less understandable': [
-      'a{border-color:rgba(0,0,0,.5);border-color:red}',
-      'a{border-color:red}'
-    ],
-    'less understandable granular property should NOT override more understandable': [
-      'a{border-color:red;border-color:rgba(0,0,0,.5)}',
-      'a{border-color:red;border-color:rgba(0,0,0,.5)}'
-    ],
-    'two same granular properties redefined': [
-      'a{border-color:rgba(0,0,0,.5);border-color:red;border:0}',
-      'a{border:0}'
-    ],
-    'important granular property redefined': 'a{border-color:red!important;border:0}',
-    'important granular property redefined with important': [
-      'a{border-color:red!important;border:0!important}',
-      'a{border:0!important}'
-    ],
-    'mix of border properties': [
-      'a{border-top:1px solid red;border-top-color:#0f0;color:red;border-top-width:2px;border-bottom-width:1px;border:0;border-left:1px solid red}',
-      'a{color:red;border:0;border-left:1px solid red}'
-    ]
-  }),
   'grouping with advanced optimizations': cssContext({
     '@-moz-document': '@-moz-document domain(mozilla.org){a{color:red}}',
     '@media': '@media{a{color:red}}',
@@ -2450,10 +2083,11 @@ title']{display:block}",
       '.one{background:50% no-repeat}.one{background-image:url(/img.png)}',
       '.one{background:url(/img.png)50% no-repeat}'
     ],
-    'merging color with backgrounds': [
-      'p{background:red;background-image:url(1.png),url(2.png)}',
-      'p{background:url(1.png),url(2.png)red}'
-    ],
+    // TODO: restore multiplex merging
+    // 'merging color with backgrounds': [
+    //   'p{background:red;background-image:url(1.png),url(2.png)}',
+    //   'p{background:url(1.png),url(2.png)red}'
+    // ],
     'unknown @ rule': '@unknown "test";h1{color:red}',
     'property without a value': [
       'a{color:}',
diff --git a/test/properties/break-up-test.js b/test/properties/break-up-test.js
new file mode 100644 (file)
index 0000000..dd39ada
--- /dev/null
@@ -0,0 +1,697 @@
+var vows = require('vows');
+var assert = require('assert');
+
+var wrapForOptimizing = require('../../lib/properties/wrap-for-optimizing').all;
+var populateComponents = require('../../lib/properties/populate-components');
+
+var breakUp = require('../../lib/properties/break-up');
+
+function _breakUp(properties) {
+  var wrapped = wrapForOptimizing(properties);
+  populateComponents(wrapped);
+
+  return wrapped[0].components;
+}
+
+vows.describe(breakUp)
+  .addBatch({
+    'background': {
+      'inherit': {
+        'topic': function () {
+          return _breakUp([[['background'], ['inherit']]]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has background-image': function (components) {
+          assert.deepEqual(components[0].name, 'background-image');
+          assert.deepEqual(components[0].value, [['inherit']]);
+        },
+        'has background-position': function (components) {
+          assert.deepEqual(components[1].name, 'background-position');
+          assert.deepEqual(components[1].value, [['inherit']]);
+        },
+        'has background-size': function (components) {
+          assert.deepEqual(components[2].name, 'background-size');
+          assert.deepEqual(components[2].value, [['inherit']]);
+        },
+        'has background-repeat': function (components) {
+          assert.deepEqual(components[3].name, 'background-repeat');
+          assert.deepEqual(components[3].value, [['inherit']]);
+        },
+        'has background-attachment': function (components) {
+          assert.deepEqual(components[4].name, 'background-attachment');
+          assert.deepEqual(components[4].value, [['scroll']]);
+        },
+        'has background-origin': function (components) {
+          assert.deepEqual(components[5].name, 'background-origin');
+          assert.deepEqual(components[5].value, [['inherit']]);
+        },
+        'has background-clip': function (components) {
+          assert.deepEqual(components[6].name, 'background-clip');
+          assert.deepEqual(components[6].value, [['inherit']]);
+        },
+        'has background-color': function (components) {
+          assert.deepEqual(components[7].name, 'background-color');
+          assert.deepEqual(components[7].value, [['inherit']]);
+        }
+      },
+      'all': {
+        'topic': function () {
+          return _breakUp([[['background'], ['__ESCAPED_URL_CLEAN_CSS0__'], ['repeat'], ['no-repeat'], ['2px'], ['3px'], ['/'], ['50%'], ['60%'], ['fixed'], ['padding-box'], ['border-box'], ['red']]]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has background-image': function (components) {
+          assert.deepEqual(components[0].name, 'background-image');
+          assert.deepEqual(components[0].value, [['__ESCAPED_URL_CLEAN_CSS0__']]);
+        },
+        'has background-position': function (components) {
+          assert.deepEqual(components[1].name, 'background-position');
+          assert.deepEqual(components[1].value, [['2px'], ['3px']]);
+        },
+        'has background-size': function (components) {
+          assert.deepEqual(components[2].name, 'background-size');
+          assert.deepEqual(components[2].value, [['50%'], ['60%']]);
+        },
+        'has background-repeat': function (components) {
+          assert.deepEqual(components[3].name, 'background-repeat');
+          assert.deepEqual(components[3].value, [['repeat'], ['no-repeat']]);
+        },
+        'has background-attachment': function (components) {
+          assert.deepEqual(components[4].name, 'background-attachment');
+          assert.deepEqual(components[4].value, [['fixed']]);
+        },
+        'has background-origin': function (components) {
+          assert.deepEqual(components[5].name, 'background-origin');
+          assert.deepEqual(components[5].value, [['padding-box']]);
+        },
+        'has background-clip': function (components) {
+          assert.deepEqual(components[6].name, 'background-clip');
+          assert.deepEqual(components[6].value, [['border-box']]);
+        },
+        'has background-color': function (components) {
+          assert.deepEqual(components[7].name, 'background-color');
+          assert.deepEqual(components[7].value, [['red']]);
+        }
+      },
+      'no size': {
+        'topic': function () {
+          return _breakUp([[['background'], ['bottom']]]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has background-position': function (components) {
+          assert.deepEqual(components[1].name, 'background-position');
+          assert.deepEqual(components[1].value, [['bottom']]);
+        },
+        'has background-size': function (components) {
+          assert.deepEqual(components[2].name, 'background-size');
+          assert.deepEqual(components[2].value, [['auto']]);
+        }
+      },
+      'shorthand size & position': {
+        'topic': function () {
+          return _breakUp([[['background'], ['2px'], ['/'], ['50px']]]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has background-position': function (components) {
+          assert.deepEqual(components[1].name, 'background-position');
+          assert.deepEqual(components[1].value, [['2px']]);
+        },
+        'has background-size': function (components) {
+          assert.deepEqual(components[2].name, 'background-size');
+          assert.deepEqual(components[2].value, [['50px']]);
+        }
+      },
+      'size & position joined together': {
+        'topic': function () {
+          return _breakUp([[['background'], ['2px/50px']]]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has background-position': function (components) {
+          assert.deepEqual(components[1].name, 'background-position');
+          assert.deepEqual(components[1].value, [['2px']]);
+        },
+        'has background-size': function (components) {
+          assert.deepEqual(components[2].name, 'background-size');
+          assert.deepEqual(components[2].value, [['50px']]);
+        }
+      },
+      'size & position joined together with 4 values': {
+        'topic': function () {
+          return _breakUp([[['background'], ['5px'], ['2px/50px'], ['30px']]]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has background-position': function (components) {
+          assert.deepEqual(components[1].name, 'background-position');
+          assert.deepEqual(components[1].value, [['5px'], ['2px']]);
+        },
+        'has background-size': function (components) {
+          assert.deepEqual(components[2].name, 'background-size');
+          assert.deepEqual(components[2].value, [['50px'], ['30px']]);
+        }
+      },
+      'clip to origin': {
+        'topic': function () {
+          return _breakUp([[['background'], ['padding-box']]]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has background-origin': function (components) {
+          assert.deepEqual(components[5].name, 'background-origin');
+          assert.deepEqual(components[5].value, [['padding-box']]);
+        },
+        'has background-clip': function (components) {
+          assert.deepEqual(components[6].name, 'background-clip');
+          assert.deepEqual(components[6].value, [['padding-box']]);
+        }
+      }
+    },
+    'border': {
+      'inherit': {
+        'topic': function () {
+          return _breakUp([[['border'], ['inherit']]]);
+        },
+        'has 3 components': function (components) {
+          assert.lengthOf(components, 3);
+        },
+        'has border-width': function (components) {
+          assert.deepEqual(components[0].name, 'border-width');
+          assert.deepEqual(components[0].value, [['inherit']]);
+        },
+        'has border-style': function (components) {
+          assert.deepEqual(components[1].name, 'border-style');
+          assert.deepEqual(components[1].value, [['inherit']]);
+        },
+        'has border-color': function (components) {
+          assert.deepEqual(components[2].name, 'border-color');
+          assert.deepEqual(components[2].value, [['inherit']]);
+        }
+      },
+      '3 inherits': {
+        'topic': function () {
+          return _breakUp([[['border'], ['inherit'], ['inherit'], ['inherit']]]);
+        },
+        'has 3 components': function (components) {
+          assert.lengthOf(components, 3);
+        },
+        'has border-width': function (components) {
+          assert.deepEqual(components[0].name, 'border-width');
+          assert.deepEqual(components[0].value, [['inherit']]);
+        },
+        'has border-style': function (components) {
+          assert.deepEqual(components[1].name, 'border-style');
+          assert.deepEqual(components[1].value, [['inherit']]);
+        },
+        'has border-color': function (components) {
+          assert.deepEqual(components[2].name, 'border-color');
+          assert.deepEqual(components[2].value, [['inherit']]);
+        }
+      },
+      'all values in correct order': {
+        'topic': function () {
+          return _breakUp([[['border'], ['1px'], ['solid'], ['red']]]);
+        },
+        'has 3 components': function (components) {
+          assert.lengthOf(components, 3);
+        },
+        'has border-width': function (components) {
+          assert.deepEqual(components[0].name, 'border-width');
+          assert.deepEqual(components[0].value, [['1px']]);
+        },
+        'has border-style': function (components) {
+          assert.deepEqual(components[1].name, 'border-style');
+          assert.deepEqual(components[1].value, [['solid']]);
+        },
+        'has border-color': function (components) {
+          assert.deepEqual(components[2].name, 'border-color');
+          assert.deepEqual(components[2].value, [['red']]);
+        }
+      },
+      'all values in wrong order': {
+        'topic': function () {
+          return _breakUp([[['border'], ['red'], ['solid'], ['1px']]]);
+        },
+        'has 3 components': function (components) {
+          assert.lengthOf(components, 3);
+        },
+        'has border-width': function (components) {
+          assert.deepEqual(components[0].name, 'border-width');
+          assert.deepEqual(components[0].value, [['1px']]);
+        },
+        'has border-style': function (components) {
+          assert.deepEqual(components[1].name, 'border-style');
+          assert.deepEqual(components[1].value, [['solid']]);
+        },
+        'has border-color': function (components) {
+          assert.deepEqual(components[2].name, 'border-color');
+          assert.deepEqual(components[2].value, [['red']]);
+        }
+      },
+      'missing values': {
+        'topic': function () {
+          return _breakUp([[['border'], ['red']]]);
+        },
+        'has 3 components': function (components) {
+          assert.lengthOf(components, 3);
+        },
+        'has border-width': function (components) {
+          assert.deepEqual(components[0].name, 'border-width');
+          assert.deepEqual(components[0].value, [['medium']]);
+        },
+        'has border-style': function (components) {
+          assert.deepEqual(components[1].name, 'border-style');
+          assert.deepEqual(components[1].value, [['none']]);
+        },
+        'has border-color': function (components) {
+          assert.deepEqual(components[2].name, 'border-color');
+          assert.deepEqual(components[2].value, [['red']]);
+        }
+      }
+    },
+    'border radius': {
+      'no horizontal vertical split': {
+        'topic': function () {
+          return _breakUp([[['border-radius'], ['0px'], ['1px'], ['2px'], ['3px']]]);
+        },
+        'has 4 components': function (components) {
+          assert.lengthOf(components, 4);
+        },
+        'has border-top-left-radius': function (components) {
+          assert.equal(components[0].name, 'border-top-left-radius');
+          assert.deepEqual(components[0].value, [['0px']]);
+        },
+        'has border-top-right-radius': function (components) {
+          assert.equal(components[1].name, 'border-top-right-radius');
+          assert.deepEqual(components[1].value, [['1px']]);
+        },
+        'has border-bottom-right-radius': function (components) {
+          assert.equal(components[2].name, 'border-bottom-right-radius');
+          assert.deepEqual(components[2].value, [['2px']]);
+        },
+        'has border-bottom-left': function (components) {
+          assert.equal(components[3].name, 'border-bottom-left-radius');
+          assert.deepEqual(components[3].value, [['3px']]);
+        }
+      },
+      'horizontal vertical split': {
+        'topic': function () {
+          return _breakUp([[['border-radius'], ['0px'], ['1px'], ['2px'], ['3px'], ['/'], ['1px'], ['2px'], ['3px'], ['4px']]]);
+        },
+        'has 4 components': function (components) {
+          assert.lengthOf(components, 4);
+        },
+        'has border-top-left-radius': function (components) {
+          assert.equal(components[0].name, 'border-top-left-radius');
+          assert.deepEqual(components[0].value, [[['0px']], [['1px']]]);
+        },
+        'has border-top-right-radius': function (components) {
+          assert.equal(components[1].name, 'border-top-right-radius');
+          assert.deepEqual(components[1].value, [[['1px']], [['2px']]]);
+        },
+        'has border-bottom-right-radius': function (components) {
+          assert.equal(components[2].name, 'border-bottom-right-radius');
+          assert.deepEqual(components[2].value, [[['2px']], [['3px']]]);
+        },
+        'has border-bottom-left': function (components) {
+          assert.equal(components[3].name, 'border-bottom-left-radius');
+          assert.deepEqual(components[3].value, [[['3px']], [['4px']]]);
+        }
+      },
+      'vendor prefix asymetrical horizontal vertical split': {
+        'topic': function () {
+          return _breakUp([[['-webkit-border-radius'], ['0px'], ['1px'], ['2px'], ['/'], ['1px'], ['4px']]]);
+        },
+        'has 4 components': function (components) {
+          assert.lengthOf(components, 4);
+        },
+        'has border-top-left-radius': function (components) {
+          assert.equal(components[0].name, '-webkit-border-top-left-radius');
+          assert.deepEqual(components[0].value, [[['0px']], [['1px']]]);
+        },
+        'has border-top-right-radius': function (components) {
+          assert.equal(components[1].name, '-webkit-border-top-right-radius');
+          assert.deepEqual(components[1].value, [[['1px']], [['4px']]]);
+        },
+        'has border-bottom-right-radius': function (components) {
+          assert.equal(components[2].name, '-webkit-border-bottom-right-radius');
+          assert.deepEqual(components[2].value, [[['2px']], [['1px']]]);
+        },
+        'has border-bottom-left': function (components) {
+          assert.equal(components[3].name, '-webkit-border-bottom-left-radius');
+          assert.deepEqual(components[3].value, [[['1px']], [['4px']]]);
+        }
+      }
+    },
+    'four values': {
+      'four given': {
+        'topic': function () {
+          return _breakUp([[['margin'], ['0px'], ['1px'], ['2px'], ['3px']]]);
+        },
+        'has 4 components': function (components) {
+          assert.lengthOf(components, 4);
+        },
+        'has margin-top': function (components) {
+          assert.equal(components[0].name, 'margin-top');
+          assert.deepEqual(components[0].value, [['0px']]);
+        },
+        'has margin-right': function (components) {
+          assert.equal(components[1].name, 'margin-right');
+          assert.deepEqual(components[1].value, [['1px']]);
+        },
+        'has margin-bottom': function (components) {
+          assert.equal(components[2].name, 'margin-bottom');
+          assert.deepEqual(components[2].value, [['2px']]);
+        },
+        'has margin-left': function (components) {
+          assert.equal(components[3].name, 'margin-left');
+          assert.deepEqual(components[3].value, [['3px']]);
+        }
+      },
+      'three given': {
+        'topic': function () {
+          return _breakUp([[['padding'], ['0px'], ['1px'], ['2px']]]);
+        },
+        'has 4 components': function (components) {
+          assert.lengthOf(components, 4);
+        },
+        'has padding-top': function (components) {
+          assert.equal(components[0].name, 'padding-top');
+          assert.deepEqual(components[0].value, [['0px']]);
+        },
+        'has padding-right': function (components) {
+          assert.equal(components[1].name, 'padding-right');
+          assert.deepEqual(components[1].value, [['1px']]);
+        },
+        'has padding-bottom': function (components) {
+          assert.equal(components[2].name, 'padding-bottom');
+          assert.deepEqual(components[2].value, [['2px']]);
+        },
+        'has padding-left': function (components) {
+          assert.equal(components[3].name, 'padding-left');
+          assert.deepEqual(components[3].value, [['1px']]);
+        }
+      },
+      'two given': {
+        'topic': function () {
+          return _breakUp([[['border-color'], ['red'], ['blue']]]);
+        },
+        'has 4 components': function (components) {
+          assert.lengthOf(components, 4);
+        },
+        'has border-top-color': function (components) {
+          assert.equal(components[0].name, 'border-top-color');
+          assert.deepEqual(components[0].value, [['red']]);
+        },
+        'has border-right-color': function (components) {
+          assert.equal(components[1].name, 'border-right-color');
+          assert.deepEqual(components[1].value, [['blue']]);
+        },
+        'has border-bottom-color': function (components) {
+          assert.equal(components[2].name, 'border-bottom-color');
+          assert.deepEqual(components[2].value, [['red']]);
+        },
+        'has border-left-color': function (components) {
+          assert.equal(components[3].name, 'border-left-color');
+          assert.deepEqual(components[3].value, [['blue']]);
+        }
+      },
+      'one given': {
+        'topic': function () {
+          return _breakUp([[['border-style'], ['solid']]]);
+        },
+        'has 4 components': function (components) {
+          assert.lengthOf(components, 4);
+        },
+        'has border-top-style': function (components) {
+          assert.equal(components[0].name, 'border-top-style');
+          assert.deepEqual(components[0].value, [['solid']]);
+        },
+        'has border-right-style': function (components) {
+          assert.equal(components[1].name, 'border-right-style');
+          assert.deepEqual(components[1].value, [['solid']]);
+        },
+        'has border-bottom-style': function (components) {
+          assert.equal(components[2].name, 'border-bottom-style');
+          assert.deepEqual(components[2].value, [['solid']]);
+        },
+        'has border-left-style': function (components) {
+          assert.equal(components[3].name, 'border-left-style');
+          assert.deepEqual(components[3].value, [['solid']]);
+        }
+      },
+      'none given': {
+        'topic': function () {
+          return _breakUp([[['border-style']]]);
+        },
+        'has 0 components': function (components) {
+          assert.lengthOf(components, 0);
+        }
+      }
+    },
+    'list style': {
+      'inherit': {
+        'topic': function () {
+          return _breakUp([[['list-style'], ['inherit']]]);
+        },
+        'has 3 components': function (components) {
+          assert.lengthOf(components, 3);
+        },
+        'has border-top-style': function (components) {
+          assert.equal(components[0].name, 'list-style-type');
+          assert.deepEqual(components[0].value, [['inherit']]);
+        },
+        'has border-right-style': function (components) {
+          assert.equal(components[1].name, 'list-style-position');
+          assert.deepEqual(components[1].value, [['inherit']]);
+        },
+        'has border-bottom-style': function (components) {
+          assert.equal(components[2].name, 'list-style-image');
+          assert.deepEqual(components[2].value, [['inherit']]);
+        }
+      },
+      'all values': {
+        'topic': function () {
+          return _breakUp([[['list-style'], ['circle'], ['inside'], ['__ESCAPED_URL_CLEAN_CSS0__']]]);
+        },
+        'has 3 components': function (components) {
+          assert.lengthOf(components, 3);
+        },
+        'has border-top-style': function (components) {
+          assert.equal(components[0].name, 'list-style-type');
+          assert.deepEqual(components[0].value, [['circle']]);
+        },
+        'has border-right-style': function (components) {
+          assert.equal(components[1].name, 'list-style-position');
+          assert.deepEqual(components[1].value, [['inside']]);
+        },
+        'has border-bottom-style': function (components) {
+          assert.equal(components[2].name, 'list-style-image');
+          assert.deepEqual(components[2].value, [['__ESCAPED_URL_CLEAN_CSS0__']]);
+        }
+      },
+      'some missing': {
+        'topic': function () {
+          return _breakUp([[['list-style'], ['inside']]]);
+        },
+        'has 3 components': function (components) {
+          assert.lengthOf(components, 3);
+        },
+        'has border-top-style': function (components) {
+          assert.equal(components[0].name, 'list-style-type');
+          assert.deepEqual(components[0].value, [['__hack']]);
+        },
+        'has border-right-style': function (components) {
+          assert.equal(components[1].name, 'list-style-position');
+          assert.deepEqual(components[1].value, [['inside']]);
+        },
+        'has border-bottom-style': function (components) {
+          assert.equal(components[2].name, 'list-style-image');
+          assert.deepEqual(components[2].value, [['none']]);
+        }
+      }
+    },
+    'multiple values': {
+      'background': {
+        'topic': function () {
+          return _breakUp([[['background'], ['__ESCAPED_URL_CLEAN_CSS0__'], ['#fff'], [','], ['url(image2.png)'], ['repeat'], ['no-repeat'], ['2px'], ['3px'], ['/'], ['50%'], ['60%'], ['fixed'], ['content-box'], ['content-box'], ['red']]]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has background-image': function (components) {
+          assert.deepEqual(components[0].name, 'background-image');
+          assert.deepEqual(components[0].value, [[['__ESCAPED_URL_CLEAN_CSS0__']], [['url(image2.png)']]]);
+          assert.isTrue(components[0].multiplex);
+        },
+        'has background-position': function (components) {
+          assert.deepEqual(components[1].name, 'background-position');
+          assert.deepEqual(components[1].value, [[['0'], ['0']], [['2px'], ['3px']]]);
+          assert.isTrue(components[0].multiplex);
+        },
+        'has background-size': function (components) {
+          assert.deepEqual(components[2].name, 'background-size');
+          assert.deepEqual(components[2].value, [[['auto']], [['50%'], ['60%']]]);
+          assert.isTrue(components[0].multiplex);
+        },
+        'has background-repeat': function (components) {
+          assert.deepEqual(components[3].name, 'background-repeat');
+          assert.deepEqual(components[3].value, [[['repeat']], [['repeat'], ['no-repeat']]]);
+          assert.isTrue(components[0].multiplex);
+        },
+        'has background-attachment': function (components) {
+          assert.deepEqual(components[4].name, 'background-attachment');
+          assert.deepEqual(components[4].value, [[['scroll']], [['fixed']]]);
+          assert.isTrue(components[0].multiplex);
+        },
+        'has background-origin': function (components) {
+          assert.deepEqual(components[5].name, 'background-origin');
+          assert.deepEqual(components[5].value, [[['padding-box']], [['content-box']]]);
+          assert.isTrue(components[0].multiplex);
+        },
+        'has background-clip': function (components) {
+          assert.deepEqual(components[6].name, 'background-clip');
+          assert.deepEqual(components[6].value, [[['border-box']], [['content-box']]]);
+          assert.isTrue(components[0].multiplex);
+        },
+        'has background-color': function (components) {
+          assert.deepEqual(components[7].name, 'background-color');
+          assert.deepEqual(components[7].value, [[['#fff']], [['red']]]);
+          assert.isTrue(components[0].multiplex);
+        }
+      }
+    },
+    'outline': {
+      'inherit': {
+        'topic': function () {
+          return _breakUp([[['outline'], ['inherit']]]);
+        },
+        'has 3 components': function (components) {
+          assert.lengthOf(components, 3);
+        },
+        'has outline-color': function (components) {
+          assert.deepEqual(components[0].name, 'outline-color');
+          assert.deepEqual(components[0].value, [['inherit']]);
+        },
+        'has outline-style': function (components) {
+          assert.deepEqual(components[1].name, 'outline-style');
+          assert.deepEqual(components[1].value, [['inherit']]);
+        },
+        'has outline-width': function (components) {
+          assert.deepEqual(components[2].name, 'outline-width');
+          assert.deepEqual(components[2].value, [['inherit']]);
+        }
+      },
+      '3 inherits': {
+        'topic': function () {
+          return _breakUp([[['outline'], ['inherit'], ['inherit'], ['inherit']]]);
+        },
+        'has 3 components': function (components) {
+          assert.lengthOf(components, 3);
+        },
+        'has outline-color': function (components) {
+          assert.deepEqual(components[0].name, 'outline-color');
+          assert.deepEqual(components[0].value, [['inherit']]);
+        },
+        'has outline-style': function (components) {
+          assert.deepEqual(components[1].name, 'outline-style');
+          assert.deepEqual(components[1].value, [['inherit']]);
+        },
+        'has outline-width': function (components) {
+          assert.deepEqual(components[2].name, 'outline-width');
+          assert.deepEqual(components[2].value, [['inherit']]);
+        }
+      },
+      'all values in correct order': {
+        'topic': function () {
+          return _breakUp([[['outline'], ['red'], ['solid'], ['1px']]]);
+        },
+        'has 3 components': function (components) {
+          assert.lengthOf(components, 3);
+        },
+        'has outline-color': function (components) {
+          assert.deepEqual(components[0].name, 'outline-color');
+          assert.deepEqual(components[0].value, [['red']]);
+        },
+        'has outline-style': function (components) {
+          assert.deepEqual(components[1].name, 'outline-style');
+          assert.deepEqual(components[1].value, [['solid']]);
+        },
+        'has outline-width': function (components) {
+          assert.deepEqual(components[2].name, 'outline-width');
+          assert.deepEqual(components[2].value, [['1px']]);
+        }
+      },
+      'all values in wrong order': {
+        'topic': function () {
+          return _breakUp([[['outline'], ['1px'], ['dotted'], ['#fff']]]);
+        },
+        'has 3 components': function (components) {
+          assert.lengthOf(components, 3);
+        },
+        'has outline-color': function (components) {
+          assert.deepEqual(components[0].name, 'outline-color');
+          assert.deepEqual(components[0].value, [['#fff']]);
+        },
+        'has outline-style': function (components) {
+          assert.deepEqual(components[1].name, 'outline-style');
+          assert.deepEqual(components[1].value, [['dotted']]);
+        },
+        'has outline-width': function (components) {
+          assert.deepEqual(components[2].name, 'outline-width');
+          assert.deepEqual(components[2].value, [['1px']]);
+        }
+      },
+      'missing values': {
+        'topic': function () {
+          return _breakUp([[['outline'], ['solid']]]);
+        },
+        'has 3 components': function (components) {
+          assert.lengthOf(components, 3);
+        },
+        'has outline-color': function (components) {
+          assert.deepEqual(components[0].name, 'outline-color');
+          assert.deepEqual(components[0].value, [['invert']]);
+        },
+        'has outline-style': function (components) {
+          assert.deepEqual(components[1].name, 'outline-style');
+          assert.deepEqual(components[1].value, [['solid']]);
+        },
+        'has outline-width': function (components) {
+          assert.deepEqual(components[2].name, 'outline-width');
+          assert.deepEqual(components[2].value, [['medium']]);
+        }
+      },
+      'default values': {
+        'topic': function () {
+          return _breakUp([[['outline'], ['invert'], ['none'], ['medium']]]);
+        },
+        'has 3 components': function (components) {
+          assert.lengthOf(components, 3);
+        },
+        'has outline-color': function (components) {
+          assert.deepEqual(components[0].name, 'outline-color');
+          assert.deepEqual(components[0].value, [['invert']]);
+        },
+        'has outline-style': function (components) {
+          assert.deepEqual(components[1].name, 'outline-style');
+          assert.deepEqual(components[1].value, [['none']]);
+        },
+        'has outline-width': function (components) {
+          assert.deepEqual(components[2].name, 'outline-width');
+          assert.deepEqual(components[2].value, [['medium']]);
+        }
+      }
+    }
+  })
+  .export(module);
diff --git a/test/properties/longhand-overriding-test.js b/test/properties/longhand-overriding-test.js
new file mode 100644 (file)
index 0000000..cccf600
--- /dev/null
@@ -0,0 +1,151 @@
+var vows = require('vows');
+var assert = require('assert');
+
+var optimize = require('../../lib/properties/optimizer');
+
+var Tokenizer = require('../../lib/selectors/tokenizer');
+var SourceTracker = require('../../lib/utils/source-tracker');
+var Compatibility = require('../../lib/utils/compatibility');
+var addOptimizationMetadata = require('../../lib/selectors/optimization-metadata');
+
+function _optimize(source) {
+  var tokens = new Tokenizer({
+    options: {},
+    sourceTracker: new SourceTracker(),
+    warnings: []
+  }).toTokens(source);
+
+  addOptimizationMetadata(tokens);
+
+  var compatibility = new Compatibility().toOptions();
+  optimize(tokens[0][1], tokens[0][2], false, { compatibility: compatibility, aggressiveMerging: true, shorthandCompacting: true });
+
+  return tokens[0][2];
+}
+
+function longhandFirst(prefixedLonghand, prefixedShorthand, zeroValue) {
+  return {
+    'topic': function () {
+      return _optimize('a{' + prefixedLonghand + ':inherit;' + prefixedShorthand + ':' + zeroValue + '}');
+    },
+    'has one token': function (body) {
+      assert.lengthOf(body, 1);
+    },
+    'has zero value only': function (body) {
+      assert.deepEqual(body[0][0], [prefixedShorthand, false, false]);
+      assert.deepEqual(body[0][1], [zeroValue]);
+    }
+  };
+}
+
+function shorthandFirst(prefixedLonghand, prefixedShorthand, zeroValue) {
+  return {
+    'topic': function () {
+      return _optimize('a{' + prefixedShorthand + ':' + zeroValue + ';' + prefixedLonghand + ':inherit}');
+    },
+    'has two tokens': function (body) {
+      assert.lengthOf(body, 2);
+    },
+    'first is shorthand': function (body) {
+      assert.deepEqual(body[0][0], [prefixedShorthand, false, false]);
+      assert.deepEqual(body[0][1], [zeroValue]);
+    },
+    'second is longhand': function (body) {
+      assert.deepEqual(body[1][0], [prefixedLonghand, false, false]);
+      assert.deepEqual(body[1][1], ['inherit']);
+    }
+  };
+}
+
+function overrideContext(longhands) {
+  var context = {};
+  var vendorPrefixes = ['', '-moz-', '-o-', '-webkit-']; // there is no -ms-animation nor -ms-transition.
+  var vendorPrefixesFor = ['animation', 'transition'];
+  var defaultValues = {
+    'list-style-image': 'none',
+    'background': '0 0'
+  };
+
+  for (var longhand in longhands) {
+    for (var i = 0; i < longhands[longhand].length; i++) {
+      var shorthand = longhands[longhand][i];
+      var prefixes = vendorPrefixesFor.indexOf(shorthand) > -1 ? vendorPrefixes : [''];
+
+      for (var j = 0, m = prefixes.length; j < m; j++) {
+        var prefixedLonghand = prefixes[j] + longhand;
+        var prefixedShorthand = prefixes[j] + shorthand;
+        var zeroValue = defaultValues[prefixedShorthand] || '0';
+
+        context['should override ' + prefixedLonghand + ' with ' + prefixedShorthand] = longhandFirst(prefixedLonghand, prefixedShorthand, zeroValue);
+        context['should not override ' + prefixedShorthand + ' shorthand with ' + prefixedLonghand] = shorthandFirst(prefixedLonghand, prefixedShorthand, zeroValue);
+      }
+    }
+  }
+
+  return context;
+}
+
+vows.describe(optimize)
+  .addBatch(
+    overrideContext({
+      'animation-delay': ['animation'],
+      'animation-direction': ['animation'],
+      'animation-duration': ['animation'],
+      'animation-fill-mode': ['animation'],
+      'animation-iteration-count': ['animation'],
+      'animation-name': ['animation'],
+      'animation-play-state': ['animation'],
+      'animation-timing-function': ['animation'],
+      'background-attachment': ['background'],
+      'background-clip': ['background'],
+      'background-color': ['background'],
+      'background-image': ['background'],
+      'background-origin': ['background'],
+      'background-position': ['background'],
+      'background-repeat': ['background'],
+      'background-size': ['background'],
+      'border-color': ['border'],
+      'border-style': ['border'],
+      'border-width': ['border'],
+      'border-bottom': ['border'],
+      'border-bottom-color': ['border-bottom', 'border-color', 'border'],
+      'border-bottom-style': ['border-bottom', 'border-style', 'border'],
+      'border-bottom-width': ['border-bottom', 'border-width', 'border'],
+      'border-left': ['border'],
+      'border-left-color': ['border-left', 'border-color', 'border'],
+      'border-left-style': ['border-left', 'border-style', 'border'],
+      'border-left-width': ['border-left', 'border-width', 'border'],
+      'border-right': ['border'],
+      'border-right-color': ['border-right', 'border-color', 'border'],
+      'border-right-style': ['border-right', 'border-style', 'border'],
+      'border-right-width': ['border-right', 'border-width', 'border'],
+      'border-top': ['border'],
+      'border-top-color': ['border-top', 'border-color', 'border'],
+      'border-top-style': ['border-top', 'border-style', 'border'],
+      'border-top-width': ['border-top', 'border-width', 'border'],
+      'font-family': ['font'],
+      'font-size': ['font'],
+      'font-style': ['font'],
+      'font-variant': ['font'],
+      'font-weight': ['font'],
+      'list-style-image': ['list-style'],
+      'list-style-position': ['list-style'],
+      'list-style-type': ['list-style'],
+      'margin-bottom': ['margin'],
+      'margin-left': ['margin'],
+      'margin-right': ['margin'],
+      'margin-top': ['margin'],
+      'outline-color': ['outline'],
+      'outline-style': ['outline'],
+      'outline-width': ['outline'],
+      'padding-bottom': ['padding'],
+      'padding-left': ['padding'],
+      'padding-right': ['padding'],
+      'padding-top': ['padding'],
+      'transition-delay': ['transition'],
+      'transition-duration': ['transition'],
+      'transition-property': ['transition'],
+      'transition-timing-function': ['transition']
+    })
+  )
+  .export(module);
diff --git a/test/properties/optimizer-test.js b/test/properties/optimizer-test.js
new file mode 100644 (file)
index 0000000..15d819e
--- /dev/null
@@ -0,0 +1,377 @@
+var vows = require('vows');
+var assert = require('assert');
+
+var optimize = require('../../lib/properties/optimizer');
+
+var Tokenizer = require('../../lib/selectors/tokenizer');
+var SourceTracker = require('../../lib/utils/source-tracker');
+var Compatibility = require('../../lib/utils/compatibility');
+var addOptimizationMetadata = require('../../lib/selectors/optimization-metadata');
+
+var compatibility = new Compatibility().toOptions();
+
+function _optimize(source, mergeAdjacent, aggressiveMerging) {
+  var tokens = new Tokenizer({
+    options: {},
+    sourceTracker: new SourceTracker(),
+    warnings: []
+  }).toTokens(source);
+
+  addOptimizationMetadata(tokens);
+  optimize(tokens[0][1], tokens[0][2], mergeAdjacent, { compatibility: compatibility, aggressiveMerging: aggressiveMerging });
+
+  return tokens[0][2];
+}
+
+vows.describe(optimize)
+  .addBatch({
+    'of two adjacent properties': {
+      'topic': 'a{display:-moz-inline-box;display:inline-block}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['display', false, false], ['-moz-inline-box']],
+          [['display', false, false], ['inline-block']]
+        ]);
+      }
+    },
+    'of two properties ': {
+      'topic': 'a{display:inline-block;color:red;display:block}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['color', false , false], ['red']],
+          [['display', false , false], ['block']]
+        ]);
+      }
+    },
+    'of two properties  where former is !important': {
+      'topic': 'a{display:inline-block!important;color:red;display:block}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['display', true , false], ['inline-block']],
+          [['color', false , false], ['red']]
+        ]);
+      }
+    },
+    'of two properties  where latter is !important': {
+      'topic': 'a{display:inline-block;color:red;display:block!important}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['color', false , false], ['red']],
+          [['display', true , false], ['block']]
+        ]);
+      }
+    },
+    'of two properties  where both are !important': {
+      'topic': 'a{display:inline-block!important;color:red;display:block!important}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['color', false , false], ['red']],
+          [['display', true , false], ['block']]
+        ]);
+      }
+    },
+    'of many properties': {
+      'topic': 'a{display:inline-block;color:red;font-weight:bolder;font-weight:700;display:block!important;color:#fff}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['font-weight', false , false], ['bolder']],
+          [['font-weight', false , false], ['700']],
+          [['display', true , false], ['block']],
+          [['color', false , false], ['#fff']]
+        ]);
+      }
+    },
+    'both redefined': {
+      'topic': 'p{display:block;display:-moz-inline-box;color:red;display:table-cell}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['color', false , false], ['red']],
+          [['display', false , false], ['table-cell']]
+        ]);
+      }
+    },
+    'filter treated as background': {
+      'topic': 'p{background:-moz-linear-gradient();background:-webkit-linear-gradient();filter:"progid:DXImageTransform";background:linear-gradient()}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['background', false , false], ['-moz-linear-gradient()']],
+          [['background', false , false], ['-webkit-linear-gradient()']],
+          [['filter', false , false], ['"progid:DXImageTransform"']],
+          [['background', false , false], ['linear-gradient()']]
+        ]);
+      }
+    },
+    'filter treated as background-image': {
+      'topic': 'p{background-image:-moz-linear-gradient();background-image:-webkit-linear-gradient();filter:"progid:DXImageTransform";background-image:linear-gradient()}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['background-image', false , false], ['-moz-linear-gradient()']],
+          [['background-image', false , false], ['-webkit-linear-gradient()']],
+          [['filter', false , false], ['"progid:DXImageTransform"']],
+          [['background-image', false , false], ['linear-gradient()']]
+        ]);
+      }
+    },
+    '-ms-filter treated as background': {
+      'topic': 'p{background:-moz-linear-gradient();background:-webkit-linear-gradient();-ms-filter:"progid:DXImageTransform";background:linear-gradient()}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['background', false , false], ['-moz-linear-gradient()']],
+          [['background', false , false], ['-webkit-linear-gradient()']],
+          [['-ms-filter', false , false], ['"progid:DXImageTransform"']],
+          [['background', false , false], ['linear-gradient()']]
+        ]);
+      }
+    },
+    '-ms-filter treated as background-image': {
+      'topic': 'p{background-image:-moz-linear-gradient();background-image:-webkit-linear-gradient();-ms-filter:"progid:DXImageTransform";background-image:linear-gradient()}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['background-image', false , false], ['-moz-linear-gradient()']],
+          [['background-image', false , false], ['-webkit-linear-gradient()']],
+          [['-ms-filter', false , false], ['"progid:DXImageTransform"']],
+          [['background-image', false , false], ['linear-gradient()']]
+        ]);
+      }
+    },
+    'longhand then shorthand': {
+      'topic': 'p{border-left-style:solid;border:1px dotted red}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['border', false , false], ['1px'], ['dotted'], ['red']]
+        ]);
+      }
+    },
+    'longhand then shorthand with important': {
+      'topic': 'p{border-left-style:solid!important;border:1px dotted red}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['border-left-style', true, false], ['solid']],
+          [['border', false , false], ['1px'], ['dotted'], ['red']]
+        ]);
+      }
+    },
+    'shorthand then longhand': {
+      'topic': 'p{background:url(image.png);background-image:#fff}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['background', false , false], ['url(image.png)']],
+          [['background-image', false , false], ['#fff']]
+        ]);
+      }
+    }
+  })
+  .addBatch({
+    'ie hacks - normal before hack': {
+      'topic': 'p{color:red;display:none;color:#fff\\9}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['color', false , false], ['red']],
+          [['display', false , false], ['none']],
+          [['color', false , true], ['#fff\\9']]
+        ]);
+      }
+    },
+    'ie hacks - normal after hack': {
+      'topic': 'p{color:red\\9;display:none;color:#fff}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['color', false , true], ['red\\9']],
+          [['display', false , false], ['none']],
+          [['color', false , false], ['#fff']]
+        ]);
+      }
+    },
+    'ie hacks - hack after hack': {
+      'topic': 'p{color:red\\9;display:none;color:#fff\\9}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['display', false , false], ['none']],
+          [['color', false , true], ['#fff\\9']]
+        ]);
+      }
+    }
+  })
+  .addBatch({
+    'mergeAdjacent is true': {
+      'topic': 'p{display:block;display:inline-block}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, true, true), [
+          [['display', false , false], ['inline-block']]
+        ]);
+      }
+    },
+    'mergeAdjacent is false': {
+      'topic': 'p{display:block;display:inline-block}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['display', false , false], ['block']],
+          [['display', false , false], ['inline-block']]
+        ]);
+      }
+    },
+    'mergeAdjacent is an array with irrelevant join positions': {
+      'topic': 'p{display:block;display:inline-block;color:red}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, [2], true), [
+          [['display', false , false], ['block']],
+          [['display', false , false], ['inline-block']],
+          [['color', false , false], ['red']]
+        ]);
+      }
+    },
+    'mergeAdjacent is an array with relevant join positions': {
+      'topic': 'p{display:block;display:inline-block;color:red}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, [1], true), [
+          [['display', false , false], ['inline-block']],
+          [['color', false , false], ['red']]
+        ]);
+      }
+    }
+  })
+  .addBatch({
+    'aggressive off - (yet) not overriddable': {
+      'topic': 'a{display:inline-block;color:red;display:-moz-block}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, false), [
+          [['display', false , false], ['inline-block']],
+          [['color', false , false], ['red']],
+          [['display', false , false], ['-moz-block']]
+        ]);
+      }
+    }
+  })
+  .addBatch({
+    'understandable - 2 properties, both !important, 2nd less understandable': {
+      'topic': 'a{color:red!important;display:block;color:rgba(0,255,0,.5)!important}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['color', true , false], ['red']],
+          [['display', false , false], ['block']],
+          [['color', true , false], ['rgba(0,255,0,.5)']]
+        ]);
+      }
+    },
+    'understandable - 2 properties, both !important, 2nd more understandable': {
+      'topic': 'a{color:rgba(0,255,0,.5)!important;display:block;color:red!important}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['display', false , false], ['block']],
+          [['color', true , false], ['red']]
+        ]);
+      }
+    },
+    'understandable - 2 adjacent properties, both !important, 2nd less understandable': {
+      'topic': 'a{background:red!important;background:rgba(0,255,0,.5)!important}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['background', true , false], ['red']],
+          [['background', true , false], ['rgba(0,255,0,.5)']]
+        ]);
+      }
+    },
+    'understandable - 2 adjacent properties, both !important, 2nd more understandable': {
+      'topic': 'a{background:rgba(0,255,0,.5)!important;background:red!important}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['background', true , false], ['rgba(0,255,0,.5)']],
+          [['background', true , false], ['red']]
+        ]);
+      }
+    },
+    'understandable - 2 adjacent -ms-transform with different values': {
+      'topic': 'div{-ms-transform:translate(0,0);-ms-transform:translate3d(0,0,0)}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['-ms-transform', false , false], ['translate(0,0)']],
+          [['-ms-transform', false , false], ['translate3d(0,0,0)']]
+        ]);
+      }
+    },
+    'understandable - 2 non-adjacent -ms-transform with different values': {
+      'topic': 'div{-ms-transform:translate(0,0);-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0)}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['-ms-transform', false , false], ['translate(0,0)']],
+          [['-webkit-transform', false , false], ['translate3d(0,0,0)']],
+          [['-ms-transform', false , false], ['translate3d(0,0,0)']]
+        ]);
+      }
+    },
+    'understandable - 2 adjacent transform with different values': {
+      'topic': 'div{transform:translate(0,0);transform:translate3d(0,0,0)}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['transform', false , false], ['translate(0,0)']],
+          [['transform', false , false], ['translate3d(0,0,0)']]
+        ]);
+      }
+    },
+    'understandable - 2 non-adjacent transform with different values': {
+      'topic': 'div{transform:translate(0,0);-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['transform', false , false], ['translate(0,0)']],
+          [['-webkit-transform', false , false], ['translate3d(0,0,0)']],
+          [['transform', false , false], ['translate3d(0,0,0)']]
+        ]);
+      }
+    },
+    'understandable - border(hex) with border(rgba)': {
+      'topic': 'a{border:1px solid #fff;border:1px solid rgba(1,0,0,.5)}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['border', false , false], ['1px'], ['solid'], ['#fff']],
+          [['border', false , false], ['1px'], ['solid'], ['rgba(1,0,0,.5)']]
+        ]);
+      }
+    },
+    'understandable - border(hex) with border(rgba !important)': {
+      'topic': 'a{border:1px solid #fff;border:1px solid rgba(1,0,0,.5)!important}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['border', false , false], ['1px'], ['solid'], ['#fff']],
+          [['border', true , false], ['1px'], ['solid'], ['rgba(1,0,0,.5)']]
+        ]);
+      }
+    },
+    'understandable - border(hex !important) with border(hex)': {
+      'topic': 'a{border:1px solid #fff!important;display:block;border:1px solid #fff}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['border', true , false], ['1px'], ['solid'], ['#fff']],
+          [['display', false , false], ['block']]
+        ]);
+      }
+    },
+    'understandable - border(hex) with border(hex !important)': {
+      'topic': 'a{border:1px solid #fff;display:block;border:1px solid #fff!important}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['display', false , false], ['block']],
+          [['border', true , false], ['1px'], ['solid'], ['#fff']]
+        ]);
+      }
+    },
+    'understandable - unit with function with unit without one': {
+      'topic': 'a{border-top-width:calc(100%);display:block;border-top-width:1px}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['display', false , false], ['block']],
+          [['border-top-width', false , false], ['1px']]
+        ]);
+      }
+    },
+    'understandable - unit without function with unit with one': {
+      'topic': 'a{border-top-width:1px;display:block;border-top-width:calc(100%)}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, false, true), [
+          [['border-top-width', false , false], ['1px']],
+          [['display', false , false], ['block']],
+          [['border-top-width', false , false], ['calc(100%)']]
+        ]);
+      }
+    }
+  })
+  .export(module);
diff --git a/test/properties/override-compacting-test.js b/test/properties/override-compacting-test.js
new file mode 100644 (file)
index 0000000..a3e9092
--- /dev/null
@@ -0,0 +1,207 @@
+var vows = require('vows');
+var assert = require('assert');
+
+var optimize = require('../../lib/properties/optimizer');
+
+var Tokenizer = require('../../lib/selectors/tokenizer');
+var SourceTracker = require('../../lib/utils/source-tracker');
+var Compatibility = require('../../lib/utils/compatibility');
+var addOptimizationMetadata = require('../../lib/selectors/optimization-metadata');
+
+function _optimize(source, compatibility) {
+  var tokens = new Tokenizer({
+    options: {},
+    sourceTracker: new SourceTracker(),
+    warnings: []
+  }).toTokens(source);
+  compatibility = new Compatibility(compatibility).toOptions();
+
+  addOptimizationMetadata(tokens);
+  optimize(tokens[0][1], tokens[0][2], false, { compatibility: compatibility, shorthandCompacting: true });
+
+  return tokens[0][2];
+}
+
+vows.describe(optimize)
+  .addBatch({
+    'longhand then shorthand': {
+      'topic': 'p{background-image:none;background:__ESCAPED_URL_CLEAN_CSS0__}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic), [
+          [['background', false , false], ['__ESCAPED_URL_CLEAN_CSS0__']]
+        ]);
+      }
+    },
+    'longhand then shorthand - important then non-important': {
+      'topic': 'p{background-image:none!important;background:__ESCAPED_URL_CLEAN_CSS0__}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic), [
+          [['background-image', true , false], ['none']],
+          [['background', false , false], ['__ESCAPED_URL_CLEAN_CSS0__']]
+        ]);
+      }
+    },
+    'longhand then shorthand - multiplex then simple': {
+      'topic': 'p{background-repeat:no-repeat,no-repeat;background:__ESCAPED_URL_CLEAN_CSS0__}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic), [
+          [['background-repeat', false , false], ['no-repeat'], [','], ['no-repeat']],
+          [['background', false , false], ['__ESCAPED_URL_CLEAN_CSS0__']]
+        ]);
+      }
+    },
+    'longhand then shorthand - simple then multiplex': {
+      'topic': 'p{background-repeat:no-repeat;background:__ESCAPED_URL_CLEAN_CSS0__,__ESCAPED_URL_CLEAN_CSS1__}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic), [
+          [['background', false , false], ['__ESCAPED_URL_CLEAN_CSS0__'], [','], ['__ESCAPED_URL_CLEAN_CSS1__']]
+        ]);
+      }
+    },
+    'shorthand then longhand': {
+      'topic': 'p{background:__ESCAPED_URL_CLEAN_CSS0__ repeat;background-repeat:no-repeat}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic), [
+          [['background', false , false], ['__ESCAPED_URL_CLEAN_CSS0__'], ['no-repeat']]
+        ]);
+      }
+    },
+    'shorthand then longhand - important then non-important': {
+      'topic': 'p{background:__ESCAPED_URL_CLEAN_CSS0__ repeat!important;background-repeat:no-repeat}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic), [
+          [['background', true , false], ['__ESCAPED_URL_CLEAN_CSS0__'], ['no-repeat']]
+        ]);
+      }
+    },
+    'shorthand then longhand - non-important then important': {
+      'topic': 'p{background:__ESCAPED_URL_CLEAN_CSS0__ repeat;background-repeat:no-repeat!important}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic), [
+          [['background', false , false], ['__ESCAPED_URL_CLEAN_CSS0__']],
+          [['background-repeat', true , false], ['no-repeat']]
+        ]);
+      }
+    },
+    'shorthand then longhand - multiple values': {
+      'topic': 'p{background:__ESCAPED_URL_CLEAN_CSS0__,__ESCAPED_URL_CLEAN_CSS1__;background-repeat:no-repeat}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic), [
+          [['background', false , false], ['__ESCAPED_URL_CLEAN_CSS0__'], ['no-repeat'], [','], ['__ESCAPED_URL_CLEAN_CSS1__'], ['no-repeat']]
+        ]);
+      }
+    },
+    'shorthand then longhand - single value then multi value': {
+      'topic': 'p{background:__ESCAPED_URL_CLEAN_CSS0__;background-repeat:no-repeat,no-repeat}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic), [
+          [['background', false , false], ['__ESCAPED_URL_CLEAN_CSS0__']],
+          [['background-repeat', false , false], ['no-repeat'], [','], ['no-repeat']]
+        ]);
+      }
+    },
+    'shorthand then longhand - disabled background size merging': {
+      'topic': 'p{background:__ESCAPED_URL_CLEAN_CSS0__;background-size:50%}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, { properties: { backgroundSizeMerging: false } }), [
+          [['background', false , false], ['__ESCAPED_URL_CLEAN_CSS0__']],
+          [['background-size', false , false], ['50%']]
+        ]);
+      }
+    },
+    'shorthand then longhand - non mergeable value': {
+      'topic': 'p{background:__ESCAPED_URL_CLEAN_CSS0__;background-color:none}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, { properties: { backgroundSizeMerging: false } }), [
+          [['background', false , false], ['__ESCAPED_URL_CLEAN_CSS0__']],
+          [['background-color', false , false], ['none']]
+        ]);
+      }
+    },
+    'shorthand then longhand - color into a function': {
+      'topic': 'p{background:linear-gradient();background-color:red}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, { properties: { backgroundSizeMerging: false } }), [
+          [['background', false , false], ['linear-gradient()'], ['red']]
+        ]);
+      }
+    },
+    'shorthand then longhand - color into a color - with merging off': {
+      'topic': 'p{background:white;background-color:red}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, { properties: { merging: false } }), [
+          [['background', false , false], ['red']]
+        ]);
+      }
+    },
+    'shorthand then longhand - color into a function - with merging off': {
+      'topic': 'p{background:linear-gradient();background-color:red}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic, { properties: { merging: false } }), [
+          [['background', false , false], ['linear-gradient()']],
+          [['background-color', false , false], ['red']]
+        ]);
+      }
+    },
+    'shorthand then shorthand - same values': {
+      'topic': 'p{background:red;background:red}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic), [
+          [['background', false , false], ['red']]
+        ]);
+      }
+    },
+    'shorthand then shorthand - same values with defaults': {
+      'topic': 'p{background:repeat red;background:red}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic), [
+          [['background', false , false], ['red']]
+        ]);
+      }
+    },
+    'shorthand then shorthand - with different functions': {
+      'topic': 'p{background:linear-gradient();background:-webkit-gradient()}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic), [
+          [['background', false , false], ['linear-gradient()']],
+          [['background', false , false], ['-webkit-gradient()']]
+        ]);
+      }
+    },
+    'shorthand then shorthand - with function and url': {
+      'topic': 'p{background:linear-gradient();background:__ESCAPED_URL_CLEAN_CSS0__}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic), [
+          [['background', false , false], ['linear-gradient()']],
+          [['background', false , false], ['__ESCAPED_URL_CLEAN_CSS0__']]
+        ]);
+      }
+    },
+    'shorthand then shorthand - important then non-important': {
+      'topic': 'p{background:__ESCAPED_URL_CLEAN_CSS0__ no-repeat!important;background:__ESCAPED_URL_CLEAN_CSS1__ repeat red}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic), [
+          [['background', true , false], ['__ESCAPED_URL_CLEAN_CSS0__'], ['no-repeat']]
+        ]);
+      }
+    },
+    'shorthand then shorthand - non-important then important': {
+      'topic': 'p{background:__ESCAPED_URL_CLEAN_CSS0__ no-repeat;background:__ESCAPED_URL_CLEAN_CSS1__ repeat red!important}',
+      'into': function (topic) {
+        assert.deepEqual(_optimize(topic), [
+          [['background', true , false], ['__ESCAPED_URL_CLEAN_CSS1__'], ['red']]
+        ]);
+      }
+    }
+
+      // 'aggressive off - overriddable': {
+      //   'topic': 'a{background:white;color:red;background:red}',
+      //   'into': function (topic) {
+      //     assert.deepEqual(optimize(topic, false, false), [
+      //       [['color', false , false], ['red']],
+      //       [['background', false , false], ['red']]
+      //     ]);
+      //   }
+      // }
+  })
+  .export(module);
diff --git a/test/properties/populate-components-test.js b/test/properties/populate-components-test.js
new file mode 100644 (file)
index 0000000..b8a48dd
--- /dev/null
@@ -0,0 +1,74 @@
+var vows = require('vows');
+var assert = require('assert');
+
+var wrapForOptimizing = require('../../lib/properties/wrap-for-optimizing').all;
+var populateComponents = require('../../lib/properties/populate-components');
+
+vows.describe(populateComponents)
+  .addBatch({
+    'shorthand': {
+      'topic': function () {
+        var wrapped = wrapForOptimizing([[['margin', false, false], ['0px'], ['1px'], ['2px'], ['3px']]]);
+
+        populateComponents(wrapped);
+        return wrapped;
+      },
+      'has one': function (wrapped) {
+        assert.lengthOf(wrapped, 1);
+      },
+      'becomes shorthand': function (wrapped) {
+        assert.isTrue(wrapped[0].shorthand);
+      },
+      'is dirty': function (wrapped) {
+        assert.isTrue(wrapped[0].dirty);
+      },
+      'gets 4 components': function (wrapped) {
+        assert.lengthOf(wrapped[0].components, 4);
+      },
+      'gets a margin-top': function (wrapped) {
+        assert.deepEqual(wrapped[0].components[0].name, 'margin-top');
+        assert.deepEqual(wrapped[0].components[0].value, [['0px']]);
+      },
+      'gets a margin-right': function (wrapped) {
+        assert.deepEqual(wrapped[0].components[1].name, 'margin-right');
+        assert.deepEqual(wrapped[0].components[1].value, [['1px']]);
+      },
+      'gets a margin-bottom': function (wrapped) {
+        assert.deepEqual(wrapped[0].components[2].name, 'margin-bottom');
+        assert.deepEqual(wrapped[0].components[2].value, [['2px']]);
+      },
+      'gets a margin-left': function (wrapped) {
+        assert.deepEqual(wrapped[0].components[3].name, 'margin-left');
+        assert.deepEqual(wrapped[0].components[3].value, [['3px']]);
+      }
+    },
+    'longhand': {
+      'topic': function () {
+        var wrapped = wrapForOptimizing([[['margin-top', false, false], ['0px']]]);
+
+        populateComponents(wrapped);
+        return wrapped;
+      },
+      'has one': function (wrapped) {
+        assert.lengthOf(wrapped, 1);
+      },
+      'gets no components': function (wrapped) {
+        assert.isEmpty(wrapped[0].components);
+      }
+    },
+    'no value': {
+      'topic': function () {
+        var wrapped = wrapForOptimizing([[['margin', false, false]]]);
+
+        populateComponents(wrapped);
+        return wrapped;
+      },
+      'has one': function (wrapped) {
+        assert.lengthOf(wrapped, 1);
+      },
+      'is unused': function (wrapped) {
+        assert.isTrue(wrapped[0].unused);
+      }
+    }
+  })
+  .export(module);
diff --git a/test/properties/remove-unused-test.js b/test/properties/remove-unused-test.js
new file mode 100644 (file)
index 0000000..59dbd7b
--- /dev/null
@@ -0,0 +1,27 @@
+var vows = require('vows');
+var assert = require('assert');
+
+var removeUnused = require('../../lib/properties/remove-unused');
+var wrapForOptimizing = require('../../lib/properties/wrap-for-optimizing').all;
+
+vows.describe(removeUnused)
+  .addBatch({
+    'it removes unused only': {
+      'topic': function () {
+        var properties = [
+          [['background'], ['none']],
+          [['color'], ['red']]
+        ];
+        var _properties = wrapForOptimizing(properties);
+        _properties[0].unused = true;
+
+        removeUnused(_properties);
+        return properties;
+      },
+      'it has one property left': function (properties) {
+        assert.lengthOf(properties, 1);
+        assert.equal(properties[0][0], 'color');
+      }
+    }
+  })
+  .export(module);
diff --git a/test/properties/reorderable-test.js b/test/properties/reorderable-test.js
deleted file mode 100644 (file)
index 4ee8787..0000000
+++ /dev/null
@@ -1,127 +0,0 @@
-var vows = require('vows');
-var assert = require('assert');
-
-var SelectorTokenizer = require('../../lib/selectors/tokenizer');
-var extractProperties = require('../../lib/properties/extractor');
-var canReorder = require('../../lib/properties/reorderable').canReorder;
-var canReorderSingle = require('../../lib/properties/reorderable').canReorderSingle;
-
-function propertiesIn(source) {
-  return extractProperties(new SelectorTokenizer({ options: {} }, false).toTokens(source)[0]);
-}
-
-vows.describe(canReorder)
-  .addBatch({
-    'empty': {
-      'topic': canReorder(propertiesIn('a{}'), propertiesIn('a{}')),
-      'must be true': function (result) { assert.isTrue(result); }
-    },
-    'left empty': {
-      'topic': canReorder(propertiesIn('a{}'), propertiesIn('a{color:red}')),
-      'must be true': function (result) { assert.isTrue(result); }
-    },
-    'right empty': {
-      'topic': canReorder(propertiesIn('a{color:red}'), propertiesIn('a{}')),
-      'must be true': function (result) { assert.isTrue(result); }
-    },
-    'all reorderable': {
-      'topic': canReorder(propertiesIn('a{color:red;width:100%}'), propertiesIn('a{display:block;height:20px}')),
-      'must be true': function (result) { assert.isTrue(result); }
-    },
-    'one not reorderable on the left': {
-      'topic': canReorder(propertiesIn('a{color:red;width:100%;display:inline}'), propertiesIn('a{display:block;height:20px}')),
-      'must be false': function (result) { assert.isFalse(result); }
-    },
-    'one not reorderable on the right': {
-      'topic': canReorder(propertiesIn('a{color:red;width:100%}'), propertiesIn('a{display:block;height:20px;width:20px}')),
-      'must be false': function (result) { assert.isFalse(result); }
-    }
-  })
-  .export(module);
-
-vows.describe(canReorderSingle)
-  .addBatch({
-    'different properties': {
-      'topic': canReorderSingle(propertiesIn('a{color:red}')[0], propertiesIn('a{display:block}')[0]),
-      'must be true': function (result) { assert.isTrue(result); }
-    },
-    'font and line-height': {
-      'topic': canReorderSingle(propertiesIn('a{font:10px}')[0], propertiesIn('a{line-height:12px}')[0]),
-      'must be false': function (result) { assert.isFalse(result); }
-    },
-    'same properties with same value': {
-      'topic': canReorderSingle(propertiesIn('a{color:red}')[0], propertiesIn('a{color:red}')[0]),
-      'must be true': function (result) { assert.isTrue(result); }
-    },
-    'same properties with same value and different case': {
-      'topic': canReorderSingle(propertiesIn('a{COLOR:red}')[0], propertiesIn('a{color:red}')[0]),
-      'must be true': function (result) { assert.isTrue(result); }
-    },
-    'same properties with different value': {
-      'topic': canReorderSingle(propertiesIn('a{color:red}')[0], propertiesIn('a{color:blue}')[0]),
-      'must be false': function (result) { assert.isFalse(result); }
-    },
-    'same properties with different value and different case': {
-      'topic': canReorderSingle(propertiesIn('a{color:red}')[0], propertiesIn('a{COLOR:blue}')[0]),
-      'must be false': function (result) { assert.isFalse(result); }
-    },
-    'different properties with same root': {
-      'topic': canReorderSingle(propertiesIn('a{text-shadow:none}')[0], propertiesIn('a{text-decoration:underline}')[0]),
-      'must be true': function (result) { assert.isTrue(result); }
-    },
-    'different properties with same root when shorthand does not reset': {
-      'topic': canReorderSingle(propertiesIn('a{border:none}')[0], propertiesIn('a{border-spacing:1px}')[0]),
-      'must be true': function (result) { assert.isTrue(result); }
-    },
-    'shorhand and longhand with different value': {
-      'topic': canReorderSingle(propertiesIn('a{margin:3px}')[0], propertiesIn('a{margin-bottom:5px}')[0]),
-      'must be false': function (result) { assert.isFalse(result); }
-    },
-    'shorhand and longhand with same value': {
-      'topic': canReorderSingle(propertiesIn('a{margin:3px}')[0], propertiesIn('a{margin-bottom:3px}')[0]),
-      'must be false': function (result) { assert.isTrue(result); }
-    },
-    'two longhand with different value sharing same shorthand': {
-      'topic': canReorderSingle(propertiesIn('a{margin-top:3px solid red}')[0], propertiesIn('a{margin-bottom:3px solid white}')[0]),
-      'must be true': function (result) { assert.isTrue(result); }
-    },
-    'different, non-overlapping simple selectors': {
-      'topic': canReorderSingle(propertiesIn('a{border:none}')[0], propertiesIn('div{border:1px solid #f00}')[0]),
-      'must be true': function (result) { assert.isTrue(result); }
-    },
-    'different, non-overlapping complex selectors': {
-      'topic': canReorderSingle(propertiesIn('.one{border:none}')[0], propertiesIn('div{border:1px solid #f00}')[0]),
-      'must be false': function (result) { assert.isFalse(result); }
-    },
-    'different, overlapping simple selectors': {
-      'topic': canReorderSingle(propertiesIn('a{border:none}')[0], propertiesIn('a{border:1px solid #f00}')[0]),
-      'must be false': function (result) { assert.isFalse(result); }
-    },
-    'align-items': {
-      'topic': canReorderSingle(propertiesIn('a{border:none}')[0], propertiesIn('a{align-items:flex-start}')[0]),
-      'must be true': function (result) { assert.isTrue(result); }
-    }
-  })
-  .addBatch({
-    'flex #1': {
-      'topic': canReorderSingle(propertiesIn('a{-webkit-box-align:flex-start}')[0], propertiesIn('a{align-items:flex-start}')[0]),
-      'must be false': function (result) { assert.isFalse(result); }
-    },
-    'flex #2': {
-      'topic': canReorderSingle(propertiesIn('a{-ms-flex-align:start}')[0], propertiesIn('a{align-items:flex-start}')[0]),
-      'must be false': function (result) { assert.isFalse(result); }
-    },
-    'flex #3': {
-      'topic': canReorderSingle(propertiesIn('a{flex:none}')[0], propertiesIn('a{align-items:flex-start}')[0]),
-      'must be false': function (result) { assert.isFalse(result); }
-    },
-    'flex #4': {
-      'topic': canReorderSingle(propertiesIn('a{justify-content:center}')[0], propertiesIn('a{–ms-flex-pack:center}')[0]),
-      'must be false': function (result) { assert.isFalse(result); }
-    },
-    'flex #5': {
-      'topic': canReorderSingle(propertiesIn('a{justify-content:center}')[0], propertiesIn('a{–webkit-box-pack:center}')[0]),
-      'must be false': function (result) { assert.isFalse(result); }
-    }
-  })
-  .export(module);
diff --git a/test/properties/restore-shorthands-test.js b/test/properties/restore-shorthands-test.js
new file mode 100644 (file)
index 0000000..07670cd
--- /dev/null
@@ -0,0 +1,41 @@
+var vows = require('vows');
+var assert = require('assert');
+
+var wrapForOptimizing = require('../../lib/properties/wrap-for-optimizing').all;
+var populateComponents = require('../../lib/properties/populate-components');
+
+var restoreShorthands = require('../../lib/properties/restore-shorthands');
+
+vows.describe(restoreShorthands)
+  .addBatch({
+    'longhands': {
+      'topic': function () {
+        var properties = ['/*comment */', [['margin-top', false, false], ['0']]];
+        var _properties = wrapForOptimizing(properties);
+        populateComponents(_properties);
+        restoreShorthands(_properties);
+
+        return properties;
+      },
+      'is same as source': function (properties) {
+        assert.deepEqual(properties, ['/*comment */', [['margin-top', false, false], ['0']]]);
+      }
+    },
+    'shorthands': {
+      'topic': function () {
+        var properties = ['/*comment */', [['background', false, false], ['url(image.png)']]];
+        var _properties = wrapForOptimizing(properties);
+        populateComponents(_properties);
+
+        properties[1].pop();
+        _properties[0].dirty = true;
+
+        restoreShorthands(_properties);
+        return properties;
+      },
+      'is same as source': function (properties) {
+        assert.deepEqual(properties, ['/*comment */', [['background', false, false], ['url(image.png)']]]);
+      }
+    }
+  })
+  .export(module);
diff --git a/test/properties/restore-test.js b/test/properties/restore-test.js
new file mode 100644 (file)
index 0000000..8563b90
--- /dev/null
@@ -0,0 +1,291 @@
+var vows = require('vows');
+var assert = require('assert');
+
+var wrapForOptimizing = require('../../lib/properties/wrap-for-optimizing').single;
+var compactable = require('../../lib/properties/compactable');
+
+var restore = require('../../lib/properties/restore');
+
+function _breakUp(property) {
+  var descriptor = compactable[property[0][0]];
+  var _property = wrapForOptimizing(property);
+  _property.components = descriptor.breakUp(_property, compactable);
+  _property.multiplex = Array.isArray(_property.components[0].value[0][0]);
+  return _property;
+}
+
+function _restore(_property) {
+  var descriptor = compactable[_property.name];
+  return descriptor.restore(_property, compactable);
+}
+
+vows.describe(restore)
+  .addBatch({
+    'background': {
+      'background with some values': {
+        'topic': function () {
+          return _restore(_breakUp([['background'], ['__ESCAPED_URL_CLEAN_CSS0__'], ['no-repeat'], ['padding-box']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['__ESCAPED_URL_CLEAN_CSS0__'], ['no-repeat'], ['padding-box']]);
+        }
+      },
+      'background with some default values': {
+        'topic': function () {
+          return _restore(_breakUp([['background'], ['__ESCAPED_URL_CLEAN_CSS0__'], ['repeat'], ['padding-box'], ['border-box']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['__ESCAPED_URL_CLEAN_CSS0__']]);
+        }
+      },
+      'background with all default values': {
+        'topic': function () {
+          return _restore(_breakUp([['background'], ['transparent'], ['none'], ['repeat'], ['scroll'], ['0'], ['0'], ['padding-box'], ['border-box']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['0 0']]);
+        }
+      },
+      'background with some double values': {
+        'topic': function () {
+          return _restore(_breakUp([['background'], ['__ESCAPED_URL_CLEAN_CSS0__'], ['repeat'], ['no-repeat'], ['2px'], ['3px'], ['/'], ['auto'], ['padding-box']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['__ESCAPED_URL_CLEAN_CSS0__'], ['2px'], ['3px'], ['repeat'], ['no-repeat'], ['padding-box']]);
+        }
+      },
+      'background with default background origin and background clip': {
+        'topic': function () {
+          return _restore(_breakUp([['background'], ['__ESCAPED_URL_CLEAN_CSS0__'], ['padding-box'], ['border-box']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['__ESCAPED_URL_CLEAN_CSS0__']]);
+        }
+      },
+      'background with same background origin and background clip': {
+        'topic': function () {
+          return _restore(_breakUp([['background'], ['__ESCAPED_URL_CLEAN_CSS0__'], ['padding-box'], ['padding-box']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['__ESCAPED_URL_CLEAN_CSS0__'], ['padding-box']]);
+        }
+      },
+      'background with default background position and background size': {
+        'topic': function () {
+          return _restore(_breakUp([['background'], ['__ESCAPED_URL_CLEAN_CSS0__'], ['0'], ['0'], ['/'], ['50%'], ['25%']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['__ESCAPED_URL_CLEAN_CSS0__'], ['0'], ['0'], ['/'], ['50%'], ['25%']]);
+        }
+      },
+      'background with default background position and single background size': {
+        'topic': function () {
+          return _restore(_breakUp([['background'], ['__ESCAPED_URL_CLEAN_CSS0__'], ['0'], ['0'], ['/'], ['50%']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['__ESCAPED_URL_CLEAN_CSS0__'], ['0'], ['0'], ['/'], ['50%']]);
+        }
+      },
+      'background with default background position and background size differing by 2nd value': {
+        'topic': function () {
+          return _restore(_breakUp([['background'], ['__ESCAPED_URL_CLEAN_CSS0__'], ['0'], ['50px'], ['/'], ['0'], ['30px']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['__ESCAPED_URL_CLEAN_CSS0__'], ['0'], ['50px'], ['/'], ['0'], ['30px']]);
+        }
+      },
+      'background 0 to background 0': {
+        'topic': function () {
+          return _restore(_breakUp([['background'], ['0']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['0']]);
+        }
+      },
+    },
+    'border radius': {
+      '4 values': {
+        'topic': function () {
+          return _restore(_breakUp([['border-radius'], ['0px'], ['1px'], ['2px'], ['3px']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['0px'], ['1px'], ['2px'], ['3px']]);
+        }
+      },
+      '3 values': {
+        'topic': function () {
+          return _restore(_breakUp([['border-radius'], ['0px'], ['1px'], ['2px'], ['1px']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['0px'], ['1px'], ['2px']]);
+        }
+      },
+      '2 values': {
+        'topic': function () {
+          return _restore(_breakUp([['border-radius'], ['0px'], ['1px'], ['0px'], ['1px']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['0px'], ['1px']]);
+        }
+      },
+      '1 value': {
+        'topic': function () {
+          return _restore(_breakUp([['border-radius'], ['0px'], ['0px'], ['0px'], ['0px']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['0px']]);
+        }
+      },
+      'horizontal + vertical - different values': {
+        'topic': function () {
+          return _restore(_breakUp([['border-radius'], ['0px'], ['1px'], ['2px'], ['3px'], ['/'], ['2px'], ['1px'], ['2px'], ['1px']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['0px'], ['1px'], ['2px'], ['3px'], ['/'], ['2px'], ['1px']]);
+        }
+      },
+      'horizontal + vertical - same values': {
+        'topic': function () {
+          return _restore(_breakUp([['border-radius'], ['0px'], ['1px'], ['2px'], ['3px'], ['/'], ['0px'], ['1px'], ['2px'], ['3px']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['0px'], ['1px'], ['2px'], ['3px']]);
+        }
+      },
+      'horizontal + vertical - asymetrical': {
+        'topic': function () {
+          return _restore(_breakUp([['border-radius'], ['0px'], ['1px'], ['2px'], ['3px'], ['/'], ['0px'], ['1px']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['0px'], ['1px'], ['2px'], ['3px'], ['/'], ['0px'], ['1px']]);
+        }
+      }
+    },
+    'four values': {
+      '4 different': {
+        'topic': function () {
+          return _restore(_breakUp([['padding'], ['0px'], ['1px'], ['2px'], ['3px']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['0px'], ['1px'], ['2px'], ['3px']]);
+        }
+      },
+      '3 different': {
+        'topic': function () {
+          return _restore(_breakUp([['padding'], ['0px'], ['1px'], ['2px'], ['1px']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['0px'], ['1px'], ['2px']]);
+        }
+      },
+      '2 different': {
+        'topic': function () {
+          return _restore(_breakUp([['padding'], ['0px'], ['1px'], ['0px'], ['1px']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['0px'], ['1px']]);
+        }
+      },
+      'all same': {
+        'topic': function () {
+          return _restore(_breakUp([['padding'], ['0px'], ['0px'], ['0px'], ['0px']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['0px']]);
+        }
+      }
+    },
+    'repeated values': {
+      'background with some values': {
+        'topic': function () {
+          return _restore(_breakUp([['background'], ['__ESCAPED_URL_CLEAN_CSS0__'], ['no-repeat'], ['padding-box'], [','], ['repeat'], ['red']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['__ESCAPED_URL_CLEAN_CSS0__'], ['no-repeat'], ['padding-box'], [','], ['red']]);
+        }
+      },
+      'background with background origin and size': {
+        'topic': function () {
+          return _restore(_breakUp([['background'], ['no-repeat'], ['padding-box'], [','], ['repeat'], ['10px'], ['10px'], ['/'], ['auto'], ['red'], [','], ['top'], ['left'], ['/'], ['30%']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['no-repeat'], ['padding-box'], [','], ['10px'], ['10px'], ['red'], [','], ['top'], ['left'], ['/'], ['30%']]);
+        }
+      }
+    },
+    'without defaults': {
+      'border with some values': {
+        'topic': function () {
+          return _restore(_breakUp([['border'], ['solid']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['solid']]);
+        }
+      },
+      'border with all values': {
+        'topic': function () {
+          return _restore(_breakUp([['border'], ['1px'], ['solid'], ['red']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['1px'], ['solid'], ['red']]);
+        }
+      },
+      'border with all defaults': {
+        'topic': function () {
+          return _restore(_breakUp([['border'], ['medium'], ['none'], ['none']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['none']]);
+        }
+      },
+      'list with some values': {
+        'topic': function () {
+          return _restore(_breakUp([['list-style'], ['circle']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['circle']]);
+        }
+      },
+      'list with all values': {
+        'topic': function () {
+          return _restore(_breakUp([['list-style'], ['circle'], ['inside'], ['url()']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['circle'], ['inside'], ['url()']]);
+        }
+      },
+      'list with some defaults': {
+        'topic': function () {
+          return _restore(_breakUp([['list-style'], ['circle'], ['outside'], ['__ESCAPED_URL_CLEAN_CSS0__']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['circle'], ['__ESCAPED_URL_CLEAN_CSS0__']]);
+        }
+      },
+      'outline with some values': {
+        'topic': function () {
+          return _restore(_breakUp([['outline'], ['dotted']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['dotted']]);
+        }
+      },
+      'outline with all values': {
+        'topic': function () {
+          return _restore(_breakUp([['outline'], ['#fff'], ['dotted'], ['1px']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['#fff'], ['dotted'], ['1px']]);
+        }
+      },
+      'outline with all defaults': {
+        'topic': function () {
+          return _restore(_breakUp([['outline'], ['invert'], ['none'], ['medium']]));
+        },
+        'gives right value back': function (restoredValue) {
+          assert.deepEqual(restoredValue, [['0']]);
+        }
+      }
+    }
+  })
+  .export(module);
diff --git a/test/properties/shorthand-compacting-test.js b/test/properties/shorthand-compacting-test.js
new file mode 100644 (file)
index 0000000..ed1b694
--- /dev/null
@@ -0,0 +1,210 @@
+// 'shorthand properties': cssContext({
+//   'shorthand background #1' : [
+//     'div{background-color:#111;background-image:url(aaa);background-repeat:repeat;background-position:0 0;background-attachment:scroll;background-size:auto;background-origin:padding-box;background-clip:border-box}',
+//     'div{background:url(aaa)#111}'
+//   ],
+//   'shorthand background #2' : [
+//     'div{background-color:#111;background-image:url(aaa);background-repeat:no-repeat;background-position:0 0;background-attachment:scroll;background-size:auto;background-origin:padding-box;background-clip:border-box}',
+//     'div{background:url(aaa)no-repeat #111}'
+//   ],
+//   '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;background-size:auto!important;background-origin:padding-box!important;background-clip:border-box!important}',
+//     'div{background:url(aaa)#111!important}'
+//   ],
+//   'shorthand important background overriding': [
+//     'a{background:url(a.jpg) !important; background-color:#fff !important}',
+//     'a{background:url(a.jpg)#fff!important}'
+//   ],
+//   'shorthand important background overriding by non-mergeable property': [
+//     'a{background:url(a.jpg) !important; background-color:#fff !important; background-size:10px 10px !important}',
+//     'a{background:url(a.jpg)#fff!important;background-size:10px 10px!important}'
+//   ],
+//   'shorthand background-repeat correctly': [
+//     'a{background:url(/image/path.png) no-repeat repeat}',
+//     'a{background:url(/image/path.png)no-repeat repeat}'
+//   ],
+//   'shorthand border-width': [
+//     '.t{border-top-width:7px;border-bottom-width:7px;border-left-width:4px;border-right-width:4px}',
+//     '.t{border-width:7px 4px}'
+//   ],
+//   'shorthand border-color #1': [
+//     '.t{border-top-color:#9fce00;border-bottom-color:#9fce00;border-left-color:#9fce00;border-right-color:#9fce00}',
+//     '.t{border-color:#9fce00}'
+//   ],
+//   'shorthand border-color #2': [
+//     '.t{border-right-color:#002;border-bottom-color:#003;border-top-color:#001;border-left-color:#004}',
+//     '.t{border-color:#001 #002 #003 #004}'
+//   ],
+//   'shorthand border-radius': [
+//     '.t{border-top-left-radius:7px;border-bottom-right-radius:6px;border-bottom-left-radius:5px;border-top-right-radius:3px}',
+//     '.t{border-radius:7px 3px 6px 5px}'
+//   ],
+//   'shorthand border-radius none': 'li{border-radius:none}',
+//   'shorthand list-style #1': [
+//     '.t{list-style-type:circle;list-style-position:outside;list-style-image:url(aaa)}',
+//     '.t{list-style:circle url(aaa)}'
+//   ],
+//   'shorthand list-style #2': [
+//     '.t{list-style-image:url(aaa);list-style-type:circle;list-style-position:inside}',
+//     '.t{list-style:circle inside url(aaa)}'
+//   ]
+// }),
+// 'cares 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:linear-gradient(whatever) #222}',
+//     'div{background:#fff;background:linear-gradient(whatever)#222}'
+//   ],
+//   '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;background-size:auto;background-origin:padding-box;background-clip:border-box}',
+//     'div{background:#111;background:linear-gradient(aaa)#111}'
+//   ]
+// }),
+// 'cares about understandability of border components': cssContext({
+//   'border(none) with border(rgba)': 'a{border:none;border:1px solid rgba(1,0,0,.5)}',
+//   'border(rgba) with border(none)': 'a{border:1px solid rgba(1,0,0,.5);border:none}',
+//   'border(hex) with border(rgba)': 'a{border:1px solid #fff;border:1px solid rgba(1,0,0,.5)}'
+// }),
+// '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;background-size:auto;background-origin:padding-box;background-clip:border-box}',
+//     'p{background-color:#9fce00!important;background:url(hello)1px 2px repeat-y}'
+//   ],
+//   '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:url(hello)#9fce00;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;;background-size:inherit;background-origin:inherit;background-clip: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;background-size:inherit;background-origin:inherit;background-clip:inherit}',
+//     '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;background-size:auto;background-clip:inherit;background-origin:padding-box;}',
+//     'p{background:1px 2px repeat-y #9fce00;background-image:inherit;background-clip: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}'
+//   ]
+// }),
+// 'complex granular properties': cssContext({
+//   'two granular properties': 'a{border-bottom:1px solid red;border-color:red}',
+//   'more understandable granular property should override less understandable': [
+//     'a{border-color:rgba(0,0,0,.5);border-color:red}',
+//     'a{border-color:red}'
+//   ],
+//   'less understandable granular property should NOT override more understandable': [
+//     'a{border-color:red;border-color:rgba(0,0,0,.5)}',
+//     'a{border-color:red;border-color:rgba(0,0,0,.5)}'
+//   ],
+//   'two same granular properties redefined': [
+//     'a{border-color:rgba(0,0,0,.5);border-color:red;border:0}',
+//     'a{border:0}'
+//   ],
+//   'important granular property redefined': 'a{border-color:red!important;border:0}',
+//   'important granular property redefined with important': [
+//     'a{border-color:red!important;border:0!important}',
+//     'a{border:0!important}'
+//   ],
+//   'mix of border properties': [
+//     'a{border-top:1px solid red;border-top-color:#0f0;color:red;border-top-width:2px;border-bottom-width:1px;border:0;border-left:1px solid red}',
+//     'a{color:red;border:0;border-left:1px solid red}'
+//   ]
+// }),
diff --git a/test/properties/wrap-for-optimizing-test.js b/test/properties/wrap-for-optimizing-test.js
new file mode 100644 (file)
index 0000000..e6d76b5
--- /dev/null
@@ -0,0 +1,103 @@
+var vows = require('vows');
+var assert = require('assert');
+
+var wrapForOptimizing = require('../../lib/properties/wrap-for-optimizing').all;
+
+vows.describe(wrapForOptimizing)
+  .addBatch({
+    'one': {
+      'topic': function () {
+        return wrapForOptimizing([[['margin', false, false], ['0'], ['0']]]);
+      },
+      'has one wrap': function (wrapped) {
+        assert.lengthOf(wrapped, 1);
+      },
+      'has name': function (wrapped) {
+        assert.deepEqual(wrapped[0].name, 'margin');
+      },
+      'has value': function (wrapped) {
+        assert.deepEqual(wrapped[0].value, [['0'], ['0']]);
+      },
+      'has no components': function (wrapped) {
+        assert.lengthOf(wrapped[0].components, 0);
+      },
+      'is not important': function (wrapped) {
+        assert.isFalse(wrapped[0].important);
+      },
+      'is not dirty': function (wrapped) {
+        assert.isFalse(wrapped[0].dirty);
+      },
+      'is not a shorthand': function (wrapped) {
+        assert.isFalse(wrapped[0].shorthand);
+      },
+      'is not irrelevant': function (wrapped) {
+        assert.isFalse(wrapped[0].irrelevant);
+      },
+      'is real': function (wrapped) {
+        assert.isTrue(wrapped[0].real);
+      },
+      'is unused': function (wrapped) {
+        assert.isFalse(wrapped[0].unused);
+      },
+      'is hack': function (wrapped) {
+        assert.isFalse(wrapped[0].hack);
+      },
+      'is multiplex': function (wrapped) {
+        assert.isFalse(wrapped[0].multiplex);
+      }
+    },
+    'two': {
+      'topic': function () {
+        return wrapForOptimizing([[['margin', false, false], ['0'], ['0']], [['color', true, true], ['red']]]);
+      },
+      'has two wraps': function (wrapped) {
+        assert.lengthOf(wrapped, 2);
+      },
+      'marks second as important': function (wrapped) {
+        assert.isTrue(wrapped[1].important);
+      },
+      'marks second as a hack': function (wrapped) {
+        assert.isTrue(wrapped[1].hack);
+      }
+    },
+    'with comments': {
+      'topic': function () {
+        return wrapForOptimizing([['/* comment */'], [['color', true, true], ['red']]]);
+      },
+      'has one wrap': function (wrapped) {
+        assert.lengthOf(wrapped, 1);
+      },
+      'sets position correctly': function (wrapped) {
+        assert.equal(wrapped[0].position, 1);
+      }
+    },
+    'longhand': {
+      'topic': function () {
+        return wrapForOptimizing([[['border-radius-top-left', false, false], ['1px'], ['/'], ['2px']]]);
+      },
+      'has one wrap': function (wrapped) {
+        assert.lengthOf(wrapped, 1);
+      },
+      'has name': function (wrapped) {
+        assert.deepEqual(wrapped[0].name, 'border-radius-top-left');
+      },
+      'has value': function (wrapped) {
+        assert.deepEqual(wrapped[0].value, [['1px'], ['/'], ['2px']]);
+      },
+      'is multiplex': function (wrapped) {
+        assert.isTrue(wrapped[0].multiplex);
+      }
+    },
+    'without value': {
+      'topic': function () {
+        return wrapForOptimizing([[['margin', false, false]]]);
+      },
+      'has one wrap': function (wrapped) {
+        assert.lengthOf(wrapped, 1);
+      },
+      'is unused': function (wrapped) {
+        assert.isTrue(wrapped[0].unused);
+      }
+    }
+  })
+  .export(module);
similarity index 56%
rename from test/properties/extractor-test.js
rename to test/selectors/extractor-test.js
index 56f2191..b24c94c 100644 (file)
@@ -1,10 +1,10 @@
 var vows = require('vows');
 var assert = require('assert');
 var SelectorTokenizer = require('../../lib/selectors/tokenizer');
-var extractor = require('../../lib/properties/extractor');
+var extractor = require('../../lib/selectors/extractor');
 
 function buildToken(source) {
-  return new SelectorTokenizer({ options: {} }, false).toTokens(source)[0];
+  return new SelectorTokenizer({ options: {} }).toTokens(source)[0];
 }
 
 vows.describe(extractor)
@@ -24,21 +24,27 @@ vows.describe(extractor)
     'one property': {
       'topic': extractor(buildToken('a{color:red}')),
       'has no properties': function (tokens) {
-        assert.deepEqual(tokens, [['color', 'red', 'color', ['color:red'], [['a']], true]]);
+        assert.deepEqual(tokens, [['color', 'red', 'color', [['color'], ['red']], 'color:red', [['a']], true]]);
+      }
+    },
+    'one important property': {
+      'topic': extractor(buildToken('a{color:red!important}')),
+      'has no properties': function (tokens) {
+        assert.deepEqual(tokens, [['color', 'red!important', 'color', [['color'], ['red!important']], 'color:red!important', [['a']], true]]);
       }
     },
     'one property - complex selector': {
       'topic': extractor(buildToken('.one{color:red}')),
       'has no properties': function (tokens) {
-        assert.deepEqual(tokens, [['color', 'red', 'color', ['color:red'], [['.one']], false]]);
+        assert.deepEqual(tokens, [['color', 'red', 'color', [['color'], ['red']], 'color:red', [['.one']], false]]);
       }
     },
     'two properties': {
       'topic': extractor(buildToken('a{color:red;display:block}')),
       'has no properties': function (tokens) {
         assert.deepEqual(tokens, [
-          ['color', 'red', 'color', ['color:red'], [['a']], true],
-          ['display', 'block', 'display', ['display:block'], [['a']], true]
+          ['color', 'red', 'color', [['color'], ['red']], 'color:red', [['a']], true],
+          ['display', 'block', 'display', [['display'], ['block']], 'display:block', [['a']], true]
         ]);
       }
     },
@@ -46,9 +52,17 @@ vows.describe(extractor)
       'topic': extractor(buildToken('@media{a{color:red;display:block}p{color:red}}')),
       'has no properties': function (tokens) {
         assert.deepEqual(tokens, [
-          ['color', 'red', 'color', ['color:red'], [['a']], true],
-          ['display', 'block', 'display', ['display:block'], [['a']], true],
-          ['color', 'red', 'color', ['color:red'], [['p']], true]
+          ['color', 'red', 'color', [['color'], ['red']], 'color:red', [['a']], true],
+          ['display', 'block', 'display', [['display'], ['block']], 'display:block', [['a']], true],
+          ['color', 'red', 'color', [['color'], ['red']], 'color:red', [['p']], true]
+        ]);
+      }
+    },
+    'with source map info': {
+      'topic': extractor(['selector', [['a', 1, 0, undefined]], [[['color', false, false, 1, 3, undefined], ['red', 1, 9, undefined]]]]),
+      'has one property': function (tokens) {
+        assert.deepEqual(tokens, [
+          ['color', 'red', 'color', [['color', false, false, 1, 3, undefined], ['red', 1, 9, undefined]], 'color:red', [['a', 1, 0, undefined]], true],
         ]);
       }
     }
@@ -58,43 +72,43 @@ vows.describe(extractor)
       'vendor prefix': {
         'topic': extractor(buildToken('a{-moz-transform:none}')),
         'has no properties': function (tokens) {
-          assert.deepEqual(tokens, [['-moz-transform', 'none', 'transform', ['-moz-transform:none'], [['a']], true]]);
+          assert.deepEqual(tokens, [['-moz-transform', 'none', 'transform', [['-moz-transform'], ['none']], '-moz-transform:none', [['a']], true]]);
         }
       },
       'list-style': {
         'topic': extractor(buildToken('a{list-style:none}')),
         'has no properties': function (tokens) {
-          assert.deepEqual(tokens, [['list-style', 'none', 'list-style', ['list-style:none'], [['a']], true]]);
+          assert.deepEqual(tokens, [['list-style', 'none', 'list-style', [['list-style'], ['none']], 'list-style:none', [['a']], true]]);
         }
       },
       'border-radius': {
         'topic': extractor(buildToken('a{border-top-left-radius:none}')),
         'has no properties': function (tokens) {
-          assert.deepEqual(tokens, [['border-top-left-radius', 'none', 'border-radius', ['border-top-left-radius:none'], [['a']], true]]);
+          assert.deepEqual(tokens, [['border-top-left-radius', 'none', 'border-radius', [['border-top-left-radius'], ['none']], 'border-top-left-radius:none', [['a']], true]]);
         }
       },
       'vendor prefixed border-radius': {
         'topic': extractor(buildToken('a{-webkit-border-top-left-radius:none}')),
         'has no properties': function (tokens) {
-          assert.deepEqual(tokens, [['-webkit-border-top-left-radius', 'none', 'border-radius', ['-webkit-border-top-left-radius:none'], [['a']], true]]);
+          assert.deepEqual(tokens, [['-webkit-border-top-left-radius', 'none', 'border-radius', [['-webkit-border-top-left-radius'], ['none']], '-webkit-border-top-left-radius:none', [['a']], true]]);
         }
       },
       'border-image': {
         'topic': extractor(buildToken('a{border-image-width:2px}')),
         'has no properties': function (tokens) {
-          assert.deepEqual(tokens, [['border-image-width', '2px', 'border-image', ['border-image-width:2px'], [['a']], true]]);
+          assert.deepEqual(tokens, [['border-image-width', '2px', 'border-image', [['border-image-width'], ['2px']], 'border-image-width:2px', [['a']], true]]);
         }
       },
       'border-top': {
         'topic': extractor(buildToken('a{border-top-style:none}')),
         'has no properties': function (tokens) {
-          assert.deepEqual(tokens, [['border-top-style', 'none', 'border-top', ['border-top-style:none'], [['a']], true]]);
+          assert.deepEqual(tokens, [['border-top-style', 'none', 'border-top', [['border-top-style'], ['none']], 'border-top-style:none', [['a']], true]]);
         }
       },
       'text-shadow': {
         'topic': extractor(buildToken('a{text-shadow:none}')),
         'has no properties': function (tokens) {
-          assert.deepEqual(tokens, [['text-shadow', 'none', 'text-shadow', ['text-shadow:none'], [['a']], true]]);
+          assert.deepEqual(tokens, [['text-shadow', 'none', 'text-shadow', [['text-shadow'], ['none']], 'text-shadow:none', [['a']], true]]);
         }
       }
     }
diff --git a/test/selectors/optimization-metadata-test.js b/test/selectors/optimization-metadata-test.js
new file mode 100644 (file)
index 0000000..1fb9c61
--- /dev/null
@@ -0,0 +1,67 @@
+var vows = require('vows');
+var assert = require('assert');
+
+var addOptimizationMetadata = require('../../lib/selectors/optimization-metadata');
+
+vows.describe(addOptimizationMetadata)
+  .addBatch({
+    'comment': {
+      'topic': [['selector', ['a'], ['/* comment */']]],
+      'metadata': function (tokens) {
+        addOptimizationMetadata(tokens);
+        assert.deepEqual(tokens, [['selector', ['a'], ['/* comment */']]]);
+      }
+    },
+    'normal': {
+      'topic': [['selector', ['a'], [[['color'], ['red']]] ]],
+      'metadata': function (tokens) {
+        addOptimizationMetadata(tokens);
+        assert.deepEqual(tokens, [['selector', ['a'], [[['color', false, false], ['red']]] ]]);
+      }
+    },
+    'important': {
+      'topic': [['selector', ['a'], [[['color'], ['red!important']]] ]],
+      'metadata': function (tokens) {
+        addOptimizationMetadata(tokens);
+        assert.deepEqual(tokens, [['selector', ['a'], [[['color', true, false], ['red']]] ]]);
+      }
+    },
+    'underscore hack': {
+      'topic': [['selector', ['a'], [[['_color'], ['red']]] ]],
+      'metadata': function (tokens) {
+        addOptimizationMetadata(tokens);
+        assert.deepEqual(tokens, [['selector', ['a'], [[['_color', false, true], ['red']]] ]]);
+      }
+    },
+    'star hack': {
+      'topic': [['selector', ['a'], [[['_color'], ['red']]] ]],
+      'metadata': function (tokens) {
+        addOptimizationMetadata(tokens);
+        assert.deepEqual(tokens, [['selector', ['a'], [[['_color', false, true], ['red']]] ]]);
+      }
+    },
+    'backslash hack': {
+      'topic': [['selector', ['a'], [[['color'], ['red\\9']]] ]],
+      'metadata': function (tokens) {
+        addOptimizationMetadata(tokens);
+        assert.deepEqual(tokens, [['selector', ['a'], [[['color', false, true], ['red\\9']]] ]]);
+      }
+    },
+    'backslash hack - value of length 1': {
+      'topic': [['selector', ['a'], [[['width'], ['0']]] ]],
+      'metadata': function (tokens) {
+        addOptimizationMetadata(tokens);
+        assert.deepEqual(tokens, [['selector', ['a'], [[['width', false, false], ['0']]] ]]);
+      }
+    }
+  })
+  .addBatch({
+    'source map': {
+      'topic': [['selector', ['a', 1, 0, undefined], [[['color', 1, 2, undefined], ['red', 1, 2, undefined]]] ]],
+      'metadata': function (tokens) {
+        addOptimizationMetadata(tokens);
+        assert.deepEqual(tokens, [['selector', ['a', 1, 0, undefined], [[['color', false, false, 1, 2, undefined], ['red', 1, 2, undefined]]] ]]);
+      }
+    }
+  })
+  .export(module);
index 8ca2274..fd8e59a 100644 (file)
@@ -291,7 +291,7 @@ vows.describe(SelectorsOptimizer)
       ],
       'non-adjacent with multi selectors': [
         'a{padding:10px;margin:0;color:red}.one{color:red}a,p{color:red;padding:0}',
-        '.one,a,p{color:red}a{padding:10px;margin:0}a,p{padding:0}'
+        'a,p{padding:0}.one,a,p{color:red}a{margin:0}'
       ]
     }, { advanced: true, aggressiveMerging: false })
   )
index 19548fa..61d6b03 100644 (file)
@@ -4,6 +4,7 @@ var assert = require('assert');
 var Tokenizer = require('../../../lib/selectors/tokenizer');
 var SimpleOptimizer = require('../../../lib/selectors/optimizers/simple');
 var Compatibility = require('../../../lib/utils/compatibility');
+var addOptimizationMetadata = require('../../../lib/selectors/optimization-metadata');
 
 function selectorContext(group, specs, options) {
   var context = {};
@@ -37,8 +38,16 @@ function propertyContext(group, specs, options) {
   function optimized(selectors) {
     return function (source) {
       var tokens = new Tokenizer({ options: {} }).toTokens(source);
+      addOptimizationMetadata(tokens);
       new SimpleOptimizer(options).optimize(tokens);
-      var value = tokens[0] ? tokens[0][2].map(function (property) { return property[0]; }) : null;
+
+      var value = tokens[0] ?
+        tokens[0][2].map(function (property) {
+          return typeof property == 'string' ?
+            property :
+            property.map(function(t) { return t[0]; });
+        }) :
+        null;
 
       assert.deepEqual(value, selectors);
     };
@@ -123,15 +132,15 @@ vows.describe(SimpleOptimizer)
     propertyContext('@background', {
       'none to 0 0': [
         'a{background:none}',
-        ['background:0 0']
+        [['background', '0 0']]
       ],
       'transparent to 0 0': [
         'a{background:transparent}',
-        ['background:0 0']
+        [['background', '0 0']]
       ],
       'any other': [
         'a{background:red}',
-        ['background:red']
+        [['background', 'red']]
       ]
     })
   )
@@ -139,11 +148,11 @@ vows.describe(SimpleOptimizer)
     propertyContext('@border-*-radius', {
       'spaces around /': [
         'a{border-top-left-radius:2em  /  1em}',
-        ['border-top-left-radius:2em/1em']
+        [['border-top-left-radius', '2em', '/', '1em']]
       ],
       'symmetric expanded to shorthand': [
         'a{border-top-left-radius:1em 2em 3em 4em / 1em 2em 3em 4em}',
-        ['border-top-left-radius:1em 2em 3em 4em']
+        [['border-top-left-radius', '1em', '2em', '3em', '4em']]
       ]
     })
   )
@@ -151,11 +160,11 @@ vows.describe(SimpleOptimizer)
     propertyContext('@box-shadow', {
       'four zeros': [
         'a{box-shadow:0 0 0 0}',
-        ['box-shadow:0 0']
+        [['box-shadow', '0', '0']]
       ],
       'four zeros in vendor prefixed': [
         'a{-webkit-box-shadow:0 0 0 0}',
-        ['-webkit-box-shadow:0 0']
+        [['-webkit-box-shadow', '0', '0']]
       ]
     })
   )
@@ -163,67 +172,67 @@ vows.describe(SimpleOptimizer)
     propertyContext('colors', {
       'rgb to hex': [
         'a{color:rgb(255,254,253)}',
-        ['color:#fffefd']
+        [['color', '#fffefd']]
       ],
       'rgba not to hex': [
         'a{color:rgba(255,254,253,.5)}',
-        ['color:rgba(255,254,253,.5)']
+        [['color', 'rgba(255,254,253,.5)']]
       ],
       'hsl to hex': [
         'a{color:hsl(240,100%,50%)}',
-        ['color:#00f']
+        [['color', '#00f']]
       ],
       'hsla not to hex': [
         'a{color:hsla(240,100%,50%,.5)}',
-        ['color:hsla(240,100%,50%,.5)']
+        [['color', 'hsla(240,100%,50%,.5)']]
       ],
       'long hex to short hex': [
         'a{color:#ff00ff}',
-        ['color:#f0f']
+        [['color', '#f0f']]
       ],
       'hex to name': [
         'a{color:#f00}',
-        ['color:red']
+        [['color', 'red']]
       ],
       'name to hex': [
         'a{color:white}',
-        ['color:#fff']
+        [['color', '#fff']]
       ],
       'transparent black rgba to transparent': [
         'a{color:rgba(0,0,0,0)}',
-        ['color:transparent']
+        [['color', 'transparent']]
       ],
       'transparent non-black rgba': [
         'a{color:rgba(255,0,0,0)}',
-        ['color:rgba(255,0,0,0)']
+        [['color', 'rgba(255,0,0,0)']]
       ],
       'transparent black hsla to transparent': [
         'a{color:hsla(0,0%,0%,0)}',
-        ['color:transparent']
+        [['color', 'transparent']]
       ],
       'transparent non-black hsla': [
         'a{color:rgba(240,0,0,0)}',
-        ['color:rgba(240,0,0,0)']
+        [['color', 'rgba(240,0,0,0)']]
       ],
       'partial hex to name': [
         'a{color:#f00000}',
-        ['color:#f00000']
+        [['color', '#f00000']]
       ],
       'partial hex further down to name': [
         'a{background:url(test.png) #f00000}',
-        ['background:url(test.png) #f00000']
+        [['background', 'url(test.png)', '#f00000']]
       ],
       'partial name to hex': [
         'a{color:greyish}',
-        ['color:greyish']
+        [['color', 'greyish']]
       ],
       'partial name further down to hex': [
         'a{background:url(test.png) blueish}',
-        ['background:url(test.png) blueish']
+        [['background', 'url(test.png)', 'blueish']]
       ],
       'partial name as a suffix': [
         'a{font-family:alrightsanslp-black}',
-        ['font-family:alrightsanslp-black']
+        [['font-family', 'alrightsanslp-black']]
       ]
     })
   )
@@ -231,19 +240,19 @@ vows.describe(SimpleOptimizer)
     propertyContext('colors - ie8 compatibility', {
       'transparent black rgba': [
         'a{color:rgba(0,0,0,0)}',
-        ['color:rgba(0,0,0,0)']
+        [['color', 'rgba(0,0,0,0)']]
       ],
       'transparent non-black rgba': [
         'a{color:rgba(255,0,0,0)}',
-        ['color:rgba(255,0,0,0)']
+        [['color', 'rgba(255,0,0,0)']]
       ],
       'transparent black hsla': [
         'a{color:hsla(0,0%,0%,0)}',
-        ['color:hsla(0,0%,0%,0)']
+        [['color', 'hsla(0,0%,0%,0)']]
       ],
       'transparent non-black hsla': [
         'a{color:rgba(240,0,0,0)}',
-        ['color:rgba(240,0,0,0)']
+        [['color', 'rgba(240,0,0,0)']]
       ]
     }, { compatibility: 'ie8' })
   )
@@ -251,19 +260,19 @@ vows.describe(SimpleOptimizer)
     propertyContext('@filter', {
       'spaces after comma': [
         'a{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=\'#cccccc\',endColorstr=\'#000000\', enabled=true)}',
-        ['filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=\'#cccccc\', endColorstr=\'#000000\', enabled=true)']
+        [['filter', 'progid:DXImageTransform.Microsoft.gradient(startColorstr=\'#cccccc\', endColorstr=\'#000000\', enabled=true)']]
       ],
       'single Alpha filter': [
         'a{filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80)}',
-        ['filter:alpha(Opacity=80)']
+        [['filter', 'alpha(Opacity=80)']]
       ],
       'single Chroma filter': [
         'a{filter:progid:DXImageTransform.Microsoft.Chroma(color=#919191)}',
-        ['filter:chroma(color=#919191)']
+        [['filter', 'chroma(color=#919191)']]
       ],
       'multiple filters': [
         'a{filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80) progid:DXImageTransform.Microsoft.Chroma(color=#919191)}',
-        ['filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80) progid:DXImageTransform.Microsoft.Chroma(color=#919191)']
+        [['filter', 'progid:DXImageTransform.Microsoft.Alpha(Opacity=80)', 'progid:DXImageTransform.Microsoft.Chroma(color=#919191)']]
       ]
     })
   )
@@ -271,19 +280,19 @@ vows.describe(SimpleOptimizer)
     propertyContext('@font', {
       'in shorthand': [
         'a{font:normal 13px/20px sans-serif}',
-        ['font:400 13px/20px sans-serif']
+        [['font', '400', '13px', '/', '20px', 'sans-serif']]
       ],
       'in shorthand with fractions': [
         'a{font:bold .9em sans-serif}',
-        ['font:700 .9em sans-serif']
+        [['font', '700', '.9em', 'sans-serif']]
       ],
       'with font wariant and style': [
         'a{font:normal normal normal 13px/20px sans-serif}',
-        ['font:normal normal normal 13px/20px sans-serif']
+        [['font', 'normal', 'normal', 'normal', '13px', '/', '20px', 'sans-serif']]
       ],
       'with mixed order of variant and style': [
         'a{font:normal 300 normal 13px/20px sans-serif}',
-        ['font:normal 300 normal 13px/20px sans-serif']
+        [['font', 'normal', '300', 'normal', '13px', '/', '20px', 'sans-serif']]
       ]
     })
   )
@@ -291,15 +300,15 @@ vows.describe(SimpleOptimizer)
     propertyContext('@font-weight', {
       'normal to 400': [
         'a{font-weight:normal}',
-        ['font-weight:400']
+        [['font-weight', '400']]
       ],
       'bold to 700': [
         'a{font-weight:bold}',
-        ['font-weight:700']
+        [['font-weight', '700']]
       ],
       'any other': [
         'a{font-weight:bolder}',
-        ['font-weight:bolder']
+        [['font-weight', 'bolder']]
       ]
     })
   )
@@ -312,6 +321,10 @@ vows.describe(SimpleOptimizer)
       'star': [
         'a{*width:100px}',
         null
+      ],
+      'backslash': [
+        'a{width:100px\\9}',
+        null
       ]
     })
   )
@@ -319,11 +332,15 @@ vows.describe(SimpleOptimizer)
     propertyContext('ie hacks in compatibility mode', {
       'underscore': [
         'a{_width:100px}',
-        ['_width:100px']
+        [['_width', '100px']]
       ],
       'star': [
         'a{*width:100px}',
-        ['*width:100px']
+        [['*width', '100px']]
+      ],
+      'backslash': [
+        'a{width:100px\\9}',
+        [['width', '100px\\9',]]
       ]
     }, { compatibility: 'ie8' })
   )
@@ -331,15 +348,15 @@ vows.describe(SimpleOptimizer)
     propertyContext('important', {
       'minified': [
         'a{color:red!important}',
-        ['color:red!important']
+        [['color', 'red']]
       ],
       'space before !': [
         'a{color:red !important}',
-        ['color:red!important']
+        [['color', 'red']]
       ],
       'space after !': [
         'a{color:red! important}',
-        ['color:red!important']
+        [['color', 'red']]
       ]
     }, { compatibility: 'ie8' })
   )
@@ -347,11 +364,11 @@ vows.describe(SimpleOptimizer)
     propertyContext('@outline', {
       'none to 0': [
         'a{outline:none}',
-        ['outline:0']
+        [['outline', '0']]
       ],
       'any other': [
         'a{outline:10px}',
-        ['outline:10px']
+        [['outline', '10px']]
       ]
     })
   )
@@ -359,15 +376,15 @@ vows.describe(SimpleOptimizer)
     propertyContext('rounding', {
       'pixels': [
         'a{transform:translateY(123.31135px)}',
-        ['transform:translateY(123.311px)']
+        [['transform', 'translateY(123.311px)']]
       ],
       'percents': [
         'a{left:20.1231%}',
-        ['left:20.1231%']
+        [['left', '20.1231%']]
       ],
       'ems': [
         'a{left:1.1231em}',
-        ['left:1.1231em']
+        [['left', '1.1231em']]
       ]
     }, { roundingPrecision: 3 })
   )
@@ -375,15 +392,15 @@ vows.describe(SimpleOptimizer)
     propertyContext('rounding disabled', {
       'pixels': [
         'a{transform:translateY(123.31135px)}',
-        ['transform:translateY(123.31135px)']
+        [['transform', 'translateY(123.31135px)']]
       ],
       'percents': [
         'a{left:20.1231%}',
-        ['left:20.1231%']
+        [['left', '20.1231%']]
       ],
       'ems': [
         'a{left:1.1231em}',
-        ['left:1.1231em']
+        [['left', '1.1231em']]
       ]
     }, { roundingPrecision: -1 })
   )
@@ -391,27 +408,27 @@ vows.describe(SimpleOptimizer)
     propertyContext('units', {
       'pixels': [
         'a{width:0px}',
-        ['width:0']
+        [['width', '0']]
       ],
       'degrees': [
         'div{background:linear-gradient(0deg,red,#fff)}',
-        ['background:linear-gradient(0deg,red,#fff)']
+        [['background', 'linear-gradient(0deg,red,#fff)']]
       ],
       'degrees when not mixed': [
         'div{transform:rotate(0deg) skew(0deg)}',
-        ['transform:rotate(0) skew(0)']
+        [['transform', 'rotate(0)', 'skew(0)']]
       ],
       'non-zero degrees when not mixed': [
         'div{transform:rotate(10deg) skew(.5deg)}',
-        ['transform:rotate(10deg) skew(.5deg)']
+        [['transform', 'rotate(10deg)', 'skew(.5deg)']]
       ],
       'mixed units': [
         'a{margin:0em 0rem 0px 0pt}',
-        ['margin:0']
+        [['margin', '0']]
       ],
       'mixed vales': [
         'a{padding:10px 0em 30% 0rem}',
-        ['padding:10px 0 30% 0']
+        [['padding', '10px', '0', '30%', '0']]
       ]
     })
   )
@@ -419,15 +436,15 @@ vows.describe(SimpleOptimizer)
     propertyContext('units in compatibility mode', {
       'pixels': [
         'a{width:0px}',
-        ['width:0']
+        [['width', '0']]
       ],
       'mixed units': [
         'a{margin:0em 0rem 0px 0pt}',
-        ['margin:0 0rem 0 0']
+        [['margin', '0', '0rem', '0', '0']]
       ],
       'mixed vales': [
         'a{padding:10px 0em 30% 0rem}',
-        ['padding:10px 0 30% 0rem']
+        [['padding', '10px', '0', '30%', '0rem']]
       ]
     }, { compatibility: 'ie8' })
   )
@@ -435,79 +452,79 @@ vows.describe(SimpleOptimizer)
     propertyContext('zeros', {
       '-0 to 0': [
         'a{margin:-0}',
-        ['margin:0']
+        [['margin', '0']]
       ],
       '-0px to 0': [
         'a{margin:-0px}',
-        ['margin:0']
+        [['margin', '0']]
       ],
       '-0% to 0': [
         'a{width:-0%}',
-        ['width:0']
+        [['width', '0']]
       ],
       'missing': [
         'a{opacity:1.}',
-        ['opacity:1']
+        [['opacity', '1']]
       ],
       'multiple': [
         'a{margin:-0 -0 -0 -0}',
-        ['margin:0']
+        [['margin', '0']]
       ],
       'keeps negative non-zero': [
         'a{margin:-0.5em}',
-        ['margin:-.5em']
+        [['margin', '-.5em']]
       ],
       'inside names #1': [
         'div{animation-name:test-0-bounce}',
-        ['animation-name:test-0-bounce']
+        [['animation-name', 'test-0-bounce']]
       ],
       'inside names #2': [
         'div{animation-name:test-0bounce}',
-        ['animation-name:test-0bounce']
+        [['animation-name', 'test-0bounce']]
       ],
       'inside names #3': [
         'div{animation-name:test-0px}',
-        ['animation-name:test-0px']
+        [['animation-name', 'test-0px']]
       ],
       'strips leading from value': [
         'a{padding:010px 0015px}',
-        ['padding:10px 15px']
+        [['padding', '10px', '15px']]
       ],
       'strips leading from fractions': [
         'a{margin:-0.5em}',
-        ['margin:-.5em']
+        [['margin', '-.5em']]
       ],
       'strips trailing from opacity': [
         'a{opacity:1.0}',
-        ['opacity:1']
+        [['opacity', '1']]
       ],
       '.0 to 0': [
         'a{margin:.0 .0 .0 .0}',
-        ['margin:0']
+        [['margin', '0']]
       ],
       'fraction zeros': [
         'a{margin:10.0em 15.50em 10.01em 0.0em}',
-        ['margin:10em 15.5em 10.01em 0']
+        [['margin', '10em', '15.5em', '10.01em', '0']]
       ],
       'fraction zeros after rounding': [
         'a{margin:10.0010px}',
-        ['margin:10px']
+        [['margin', '10px']]
       ],
       'four zeros into one': [
         'a{margin:0 0 0 0}',
-        ['margin:0']
+        [['margin', '0']]
       ],
       'rect zeros': [
         'a{clip:rect(0px 0px 0px 0px)}',
-        ['clip:rect(0 0 0 0)']
+        [['clip', 'rect(0 0 0 0)']]
       ],
       'rect zeros with non-zero value': [
         'a{clip:rect(0.5% 0px  0px 0px)}',
-        ['clip:rect(.5% 0 0 0)']
+        [['clip', 'rect(.5% 0 0 0)']]
       ],
       'rect zeros with commas': [
         'a{clip:rect(0px, 0px, 0px, 0px)}',
-        ['clip:rect(0,0,0,0)']
+        [['clip', 'rect(0,0,0,0)']]
       ]
     })
   )
@@ -515,27 +532,27 @@ vows.describe(SimpleOptimizer)
     propertyContext('zeros with disabled zeroUnits', {
       '10.0em': [
         'a{margin:10.0em}',
-        ['margin:10em']
+        [['margin', '10em']]
       ],
       '0px': [
         'a{margin:0px}',
-        ['margin:0px']
+        [['margin', '0px']]
       ],
       '0px 0px': [
         'a{margin:0px 0px}',
-        ['margin:0px 0px']
+        [['margin', '0px', '0px']]
       ],
       '0deg': [
         'div{transform:rotate(0deg) skew(0deg)}',
-        ['transform:rotate(0deg) skew(0deg)']
+        [['transform', 'rotate(0deg)', 'skew(0deg)']]
       ],
       '0%': [
         'a{height:0%}',
-        ['height:0%']
+        [['height', '0%']]
       ],
       '10%': [
         'a{width:10%}',
-        ['width:10%']
+        [['width', '10%']]
       ]
     }, { compatibility: { properties: { zeroUnits: false } } })
   )
@@ -543,7 +560,7 @@ vows.describe(SimpleOptimizer)
     propertyContext('comments', {
       'comment': [
         'a{__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS0__color:red__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS1__}',
-        ['__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS0__', 'color:red', '__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS1__']
+        ['__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS0__', ['color', 'red'], '__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS1__']
       ]
     })
   )
@@ -551,24 +568,8 @@ vows.describe(SimpleOptimizer)
     propertyContext('whitespace', {
       'stripped spaces': [
         'div{text-shadow:rgba(255,1,1,.5) 1px}',
-        ['text-shadow:rgba(255,1,1,.5)1px']
+        [['text-shadow', 'rgba(255,1,1,.5)', '1px']]
       ]
     })
   )
-  .addBatch(
-    propertyContext('whitespace in compatibility mode', {
-      'stripped spaces': [
-        'div{text-shadow:rgba(255,1,1,.5) 1px}',
-        ['text-shadow:rgba(255,1,1,.5) 1px']
-      ]
-    }, { compatibility: 'ie8' })
-  )
-  .addBatch(
-    propertyContext('whitespace in compatibility mode', {
-      'stripped spaces': [
-        'div{text-shadow:rgba(255,1,1,.5) 1px}',
-        ['text-shadow:rgba(255,1,1,.5) 1px']
-      ]
-    }, { compatibility: 'ie8' })
-  )
   .export(module);
diff --git a/test/selectors/reorderable-test.js b/test/selectors/reorderable-test.js
new file mode 100644 (file)
index 0000000..05b3a1a
--- /dev/null
@@ -0,0 +1,231 @@
+var vows = require('vows');
+var assert = require('assert');
+
+var SelectorTokenizer = require('../../lib/selectors/tokenizer');
+var extractProperties = require('../../lib/selectors/extractor');
+var canReorder = require('../../lib/selectors/reorderable').canReorder;
+var canReorderSingle = require('../../lib/selectors/reorderable').canReorderSingle;
+
+function propertiesIn(source) {
+  return extractProperties(new SelectorTokenizer({ options: {} }, false).toTokens(source)[0]);
+}
+
+vows.describe(canReorder)
+  .addBatch({
+    'empty': {
+      'topic': function () {
+        return canReorder(propertiesIn('a{}'), propertiesIn('a{}'));
+      },
+      'must be true': function (result) {
+        assert.isTrue(result);
+      }
+    },
+    'left empty': {
+      'topic': function () {
+        return canReorder(propertiesIn('a{}'), propertiesIn('a{color:red}'));
+      },
+      'must be true': function (result) {
+        assert.isTrue(result);
+      }
+    },
+    'right empty': {
+      'topic': function () {
+        return canReorder(propertiesIn('a{color:red}'), propertiesIn('a{}'));
+      },
+      'must be true': function (result) {
+        assert.isTrue(result);
+      }
+    },
+    'all reorderable': {
+      'topic': function () {
+        return canReorder(propertiesIn('a{color:red;width:100%}'), propertiesIn('a{display:block;height:20px}'));
+      },
+      'must be true': function (result) {
+        assert.isTrue(result);
+      }
+    },
+    'one not reorderable on the left': {
+      'topic': function () {
+        return canReorder(propertiesIn('a{color:red;width:100%;display:inline}'), propertiesIn('a{display:block;height:20px}'));
+      },
+      'must be false': function (result) {
+        assert.isFalse(result);
+      }
+    },
+    'one not reorderable on the right': {
+      'topic': function () {
+        return canReorder(propertiesIn('a{color:red;width:100%}'), propertiesIn('a{display:block;height:20px;width:20px}'));
+      },
+      'must be false': function (result) {
+        assert.isFalse(result);
+      }
+    }
+  })
+  .export(module);
+
+vows.describe(canReorderSingle)
+  .addBatch({
+    'different properties': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{color:red}')[0], propertiesIn('a{display:block}')[0]);
+      },
+      'must be true': function (result) {
+        assert.isTrue(result);
+      }
+    },
+    'font and line-height': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{font:10px}')[0], propertiesIn('a{line-height:12px}')[0]);
+      },
+      'must be false': function (result) {
+        assert.isFalse(result);
+      }
+    },
+    'same properties with same value': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{color:red}')[0], propertiesIn('a{color:red}')[0]);
+      },
+      'must be true': function (result) {
+        assert.isTrue(result);
+      }
+    },
+    'same properties with same value and different case': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{COLOR:red}')[0], propertiesIn('a{color:red}')[0]);
+      },
+      'must be true': function (result) {
+        assert.isTrue(result);
+      }
+    },
+    'same properties with different value': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{color:red}')[0], propertiesIn('a{color:blue}')[0]);
+      },
+      'must be false': function (result) {
+        assert.isFalse(result);
+      }
+    },
+    'same properties with different value and different case': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{color:red}')[0], propertiesIn('a{COLOR:blue}')[0]);
+      },
+      'must be false': function (result) {
+        assert.isFalse(result);
+      }
+    },
+    'different properties with same root': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{text-shadow:none}')[0], propertiesIn('a{text-decoration:underline}')[0]);
+      },
+      'must be true': function (result) {
+        assert.isTrue(result);
+      }
+    },
+    'different properties with same root when shorthand does not reset': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{border:none}')[0], propertiesIn('a{border-spacing:1px}')[0]);
+      },
+      'must be true': function (result) {
+        assert.isTrue(result);
+      }
+    },
+    'shorhand and longhand with different value': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{margin:3px}')[0], propertiesIn('a{margin-bottom:5px}')[0]);
+      },
+      'must be false': function (result) {
+        assert.isFalse(result);
+      }
+    },
+    'shorhand and longhand with same value': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{margin:3px}')[0], propertiesIn('a{margin-bottom:3px}')[0]);
+      },
+      'must be false': function (result) {
+        assert.isTrue(result);
+      }
+    },
+    'two longhand with different value sharing same shorthand': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{margin-top:3px solid red}')[0], propertiesIn('a{margin-bottom:3px solid white}')[0]);
+      },
+      'must be true': function (result) {
+        assert.isTrue(result);
+      }
+    },
+    'different, non-overlapping simple selectors': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{border:none}')[0], propertiesIn('div{border:1px solid #f00}')[0]);
+      },
+      'must be true': function (result) {
+        assert.isTrue(result);
+      }
+    },
+    'different, non-overlapping complex selectors': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('.one{border:none}')[0], propertiesIn('div{border:1px solid #f00}')[0]);
+      },
+      'must be false': function (result) {
+        assert.isFalse(result);
+      }
+    },
+    'different, overlapping simple selectors': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{border:none}')[0], propertiesIn('a{border:1px solid #f00}')[0]);
+      },
+      'must be false': function (result) {
+        assert.isFalse(result);
+      }
+    },
+    'align-items': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{border:none}')[0], propertiesIn('a{align-items:flex-start}')[0]);
+      },
+      'must be true': function (result) {
+        assert.isTrue(result);
+      }
+    }
+  })
+  .addBatch({
+    'flex #1': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{-webkit-box-align:flex-start}')[0], propertiesIn('a{align-items:flex-start}')[0]);
+      },
+      'must be false': function (result) {
+        assert.isFalse(result);
+      }
+    },
+    'flex #2': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{-ms-flex-align:start}')[0], propertiesIn('a{align-items:flex-start}')[0]);
+      },
+      'must be false': function (result) {
+        assert.isFalse(result);
+      }
+    },
+    'flex #3': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{flex:none}')[0], propertiesIn('a{align-items:flex-start}')[0]);
+      },
+      'must be false': function (result) {
+        assert.isFalse(result);
+      }
+    },
+    'flex #4': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{justify-content:center}')[0], propertiesIn('a{–ms-flex-pack:center}')[0]);
+      },
+      'must be false': function (result) {
+        assert.isFalse(result);
+      }
+    },
+    'flex #5': {
+      'topic': function () {
+        return canReorderSingle(propertiesIn('a{justify-content:center}')[0], propertiesIn('a{–webkit-box-pack:center}')[0]);
+      },
+      'must be false': function (result) {
+        assert.isFalse(result);
+      }
+    }
+  })
+  .export(module);
index d0bbee8..47c2476 100644 (file)
@@ -82,7 +82,7 @@ vows.describe('source-maps/analyzer')
         [
           [
             'selector',
-            [['a', 1, 1, undefined], ['\n\ndiv', 3, 0, undefined]],
+            [['a', 1, 1, undefined], ['div', 3, 0, undefined]],
             []
           ]
         ]
@@ -102,7 +102,7 @@ vows.describe('source-maps/analyzer')
         [
           [
             'selector',
-            [['a', 1, 1, undefined], ['\n\ndiv\na', 3, 0, undefined], ['\n p', 5, 1, undefined]],
+            [['a', 1, 1, undefined], ['div\na', 3, 0, undefined], ['p', 5, 1, undefined]],
             []
           ]
         ]
@@ -127,7 +127,7 @@ vows.describe('source-maps/analyzer')
         [
           [
             'selector',
-            [['a ', 1, 0, undefined]],
+            [['a', 1, 0, undefined]],
             []
           ],
           [
@@ -152,7 +152,7 @@ vows.describe('source-maps/analyzer')
           [
             'selector',
             [['a', 1, 0, undefined]],
-            [['color:red', 1, 2, undefined]]
+            [[['color', 1, 2, undefined], ['red', 1, 8, undefined]]]
           ]
         ]
       ],
@@ -163,8 +163,8 @@ vows.describe('source-maps/analyzer')
             'selector',
             [['a', 1, 0, undefined]],
             [
-              ['color:red', 1, 2, undefined],
-              ['border:none', 1, 12, undefined]
+              [['color', 1, 2, undefined], ['red', 1, 8, undefined]],
+              [['border', 1, 12, undefined], ['none', 1, 19, undefined]]
             ]
           ]
         ]
@@ -176,9 +176,9 @@ vows.describe('source-maps/analyzer')
             'selector',
             [['a', 1, 0, undefined]],
             [
-              ['color:red', 1, 2, undefined],
-              ['border:none', 2, 0, undefined],
-              ['display:block', 5, 2, undefined]
+              [['color', 1, 2, undefined], ['red', 1, 8, undefined]],
+              [['border', 2, 0, undefined], ['none', 3, 0, undefined]],
+              [['display', 5, 2, undefined], ['block', 5, 10, undefined]]
             ]
           ]
         ]
@@ -189,12 +189,12 @@ vows.describe('source-maps/analyzer')
           [
             'selector',
             [['a', 1, 0, undefined]],
-            [['color:red', 1, 2, undefined]]
+            [[['color', 1, 2, undefined], ['red', 1, 8, undefined]]]
           ],
           [
             'selector',
             [['div', 1, 12, undefined]],
-            [['color:blue', 1, 16, undefined]]
+            [[['color', 1, 16, undefined], ['blue', 1, 22, undefined]]]
           ]
         ]
       ],
@@ -204,12 +204,12 @@ vows.describe('source-maps/analyzer')
           [
             'selector',
             [['a', 1, 0, undefined]],
-            [['color:red', 1, 2, undefined]]
+            [[['color', 1, 2, undefined], ['red', 1, 8, undefined]]]
           ],
           [
             'selector',
             [['div', 2, 1, undefined]],
-            [['color:blue', 2, 5, undefined]]
+            [[['color', 2, 5, undefined], ['blue', 2, 11, undefined]]]
           ]
         ]
       ],
@@ -219,12 +219,12 @@ vows.describe('source-maps/analyzer')
           [
             'selector',
             [['a', 1, 0, undefined]],
-            [['color:red', 1, 2, undefined]]
+            [[['color', 1, 2, undefined], ['red', 1, 8, undefined]]]
           ],
           [
             'selector',
             [['div', 3, 1, undefined]],
-            [['color:blue', 3, 5, undefined]]
+            [[['color', 3, 5, undefined], ['blue', 3, 11, undefined]]]
           ]
         ]
       ]
@@ -247,7 +247,7 @@ vows.describe('source-maps/analyzer')
           [
             'selector',
             [['a', 4, 0, undefined]],
-            [['color:red', 4, 2, undefined]]
+            [[['color', 4, 2, undefined], ['red', 4, 8, undefined]]]
           ]
         ]
       ],
@@ -261,7 +261,7 @@ vows.describe('source-maps/analyzer')
           [
             'selector',
             [['a', 1, 18, undefined]],
-            [['color:red', 1, 20, undefined]]
+            [[['color', 1, 20, undefined], ['red', 1, 26, undefined]]]
           ]
         ]
       ]
@@ -279,7 +279,7 @@ vows.describe('source-maps/analyzer')
               [
                 'selector',
                 [['a', 1, 25, undefined]],
-                [['color:red', 1, 27, undefined]]
+                [[['color', 1, 27, undefined], ['red', 1, 33, undefined]]]
               ]
             ]
           ]
@@ -295,7 +295,7 @@ vows.describe('source-maps/analyzer')
               [
                 'selector',
                 [['a', 4, 0, undefined]],
-                [['color:red', 5, 0, undefined]]
+                [[['color', 5, 0, undefined], ['red', 6, 0, undefined]]]
               ],
               [
                 'selector',
@@ -316,14 +316,14 @@ vows.describe('source-maps/analyzer')
               [
                 'selector',
                 [['a', 1, 25, undefined]],
-                [['color:red', 1, 27, undefined]]
+                [[['color', 1, 27, undefined], ['red', 1, 33, undefined]]]
               ]
             ]
           ],
           [
             'selector',
             [['p', 1, 39, undefined]],
-            [['color:red', 1, 41, undefined]]
+            [[['color', 1, 41, undefined], ['red', 1, 47, undefined]]]
           ]
         ]
       ],
@@ -334,10 +334,10 @@ vows.describe('source-maps/analyzer')
             'flat-block',
             ['@font-face', 1, 0, undefined],
             [
-              ['font-family:"Font"', 1, 11, undefined],
-              ['src:url("font.ttf")', 2, 0, undefined],
-              ['font-weight:normal', 3, 0, undefined],
-              ['font-style:normal', 3, 20, undefined]
+              [['font-family', 1, 11, undefined], ['"Font"', 1, 24, undefined]],
+              [['src', 2, 0, undefined], ['url("font.ttf")', 2, 5, undefined]],
+              [['font-weight', 3, 0, undefined], ['normal', 3, 13, undefined]],
+              [['font-style', 3, 20, undefined], ['normal', 3, 32, undefined]]
             ]
           ],
           [
@@ -354,7 +354,7 @@ vows.describe('source-maps/analyzer')
             'flat-block',
             ['@font-face', 2, 0, undefined],
             [
-              ['font-family:"Font"', 3, 1, undefined]
+              [['font-family', 3, 1, undefined], ['"Font"', 3, 14, undefined]]
             ]
           ]
         ]
@@ -392,32 +392,46 @@ vows.describe('source-maps/analyzer')
               ['div[data-type=__ESCAPED_FREE_TEXT_CLEAN_CSS0(1,3)__]', 1, 0, undefined],
               ['div[data-id=__ESCAPED_FREE_TEXT_CLEAN_CSS1(0,7)__]', 2, 5, undefined]
             ],
-            [['color:red', 2, 26, undefined]]
+            [[['color', 2, 26, undefined], ['red', 2, 32, undefined]]]
           ]
         ]
       ],
-      'in properties': [
-        'div{__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS0(2,5)__background:url(__ESCAPED_URL_CLEAN_CSS0(0,20)__);color:blue}a{font-family:__ESCAPED_FREE_TEXT_CLEAN_CSS0(1,3)__;color:red}',
+      'in properties123': [
+        'div{__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS0(2,5)__background:__ESCAPED_URL_CLEAN_CSS0(0,20)__;color:blue}a{font-family:__ESCAPED_FREE_TEXT_CLEAN_CSS0(1,3)__;color:red}',
         [
           [
             'selector',
             [['div', 1, 0, undefined]],
             [
-              ['__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS0(2,5)__', 1, 4, undefined],
-              ['background:url(__ESCAPED_URL_CLEAN_CSS0(0,20)__)', 3, 5, undefined],
-              ['color:blue', 3, 42, undefined]
+              '__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS0(2,5)__',
+              [['background', 3, 5, undefined], ['__ESCAPED_URL_CLEAN_CSS0(0,20)__', 3, 16, undefined]],
+              [['color', 3, 37, undefined], ['blue', 3, 43, undefined]]
             ]
           ],
           [
             'selector',
-            [['a', 3, 53, undefined]],
+            [['a', 3, 48, undefined]],
             [
-              ['font-family:__ESCAPED_FREE_TEXT_CLEAN_CSS0(1,3)__', 3, 55, undefined],
-              ['color:red', 4, 4, undefined]
+              [['font-family', 3, 50, undefined], ['__ESCAPED_FREE_TEXT_CLEAN_CSS0(1,3)__', 3, 62, undefined]],
+              [['color', 4, 4, undefined], ['red', 4, 10, undefined]]
             ]
           ]
         ]
       ],
+      'in properties generated for closing brace': [
+        'div{background:url(image.png)no-repeat}',
+        [
+          [
+            'selector',
+            [['div', 1, 0, undefined]],
+            [[
+              ['background', 1, 4, undefined],
+              ['url(image.png)', 1, 15, undefined],
+              ['no-repeat', 1, 29, undefined]
+            ]]
+          ]
+        ]
+      ],
       'in at-rules': [
         '@charset __ESCAPED_FREE_TEXT_CLEAN_CSS0(1, 5)__;div{}',
         [
@@ -442,7 +456,7 @@ vows.describe('source-maps/analyzer')
               [
                 'selector',
                 [['a', 3, 18, undefined]],
-                [['color:red', 3, 20, undefined]]
+                [[['color', 3, 20, undefined], ['red', 3, 26, undefined]]]
               ]
             ]
           ]
@@ -490,7 +504,7 @@ vows.describe('source-maps/analyzer')
           [
             'selector',
             [['a', 2, 0, 'two.css']],
-            [['color:red', 2, 2, 'two.css']]
+            [[['color', 2, 2, 'two.css'], ['red', 2, 8, 'two.css']]]
           ]
         ]
       ]
@@ -511,8 +525,26 @@ vows.describe('source-maps/analyzer')
         [
           [
             'selector',
-            [['div > a ', 1, 4, 'styles.less']],
-            [['color:red', 2, 2, 'styles.less']]
+            [['div > a', 1, 4, 'styles.less']],
+            [[['color', 2, 2, 'styles.less'], ['red', 2, 2, 'styles.less']]]
+          ]
+        ]
+      ],
+      'with fallback for properties': [
+        function () {
+          var tracker = new SourceTracker();
+          var reader = new SourceReader();
+          var inputTracker = new InputSourceMapTracker({ options: { inliner: {}, sourceMap: inputMap, options: {} }, errors: {}, sourceTracker: tracker });
+          inputTracker.track('', function () {});
+
+          var tokenizer = new Tokenizer({ sourceTracker: tracker, sourceReader: reader, inputSourceMapTracker: inputTracker, options: {} }, true);
+          return tokenizer.toTokens('div > a {\n  color: red red;\n}');
+        },
+        [
+          [
+            'selector',
+            [['div > a', 1, 4, 'styles.less']],
+            [[['color', 2, 2, 'styles.less'], ['red', 2, 2, 'styles.less'], ['red', 2, 2, 'styles.less']]]
           ]
         ]
       ]
index 1111958..15fef2c 100644 (file)
@@ -55,40 +55,58 @@ vows.describe(Tokenizer)
           ['selector', [['a']], []]
         ]
       ],
-      'a selector': [
+      'a selector with a property': [
         'a{color:red}',
         [
-          ['selector', [['a']], [['color:red']]]
+          ['selector', [['a']], [[['color'], ['red']]]]
+        ]
+      ],
+      'a selector with a multi value property': [
+        'a{margin:0px 2px 1px}',
+        [
+          ['selector', [['a']], [[['margin'], ['0px'], ['2px'], ['1px']]]]
         ]
       ],
       'a selector with whitespace': [
         'a {color:red;\n\ndisplay :\r\n  block }',
         [
-          ['selector', [['a ']], [['color:red'], ['display:block']]]
+          ['selector', [['a']], [[['color'], ['red']], [['display'], ['block']]]]
         ]
       ],
       'a selector with suffix whitespace': [
         'div a{color:red\r\n}',
         [
-          ['selector', [['div a']], [['color:red']]]
+          ['selector', [['div a']], [[['color'], ['red']]]]
         ]
       ],
       'a selector with whitespace in functions': [
         'a{color:rgba( 255, 255, 0, 0.5  )}',
         [
-          ['selector', [['a']], [['color:rgba(255,255,0,0.5)']]]
+          ['selector', [['a']], [[['color'], ['rgba( 255, 255, 0, 0.5  )']]]]
+        ]
+      ],
+      'a selector with functions and no whitespace breaks': [
+        'a{background:rgba(255,255,0,0.5)url(test.png)repeat no-repeat}',
+        [
+          ['selector', [['a']], [[['background'], ['rgba(255,255,0,0.5)'], ['url(test.png)'], ['repeat'], ['no-repeat']]]]
+        ]
+      ],
+      'a selector with escaped url and no whitespace breaks': [
+        'a{background:__ESCAPED_URL_CLEAN_CSS0__50px/25%}',
+        [
+          ['selector', [['a']], [[['background'], ['__ESCAPED_URL_CLEAN_CSS0__'], ['50px'], ['/'], ['25%']]]]
         ]
       ],
       'a selector with empty properties': [
         'a{color:red; ; ; ;}',
         [
-          ['selector', [['a']], [['color:red']]]
+          ['selector', [['a']], [[['color'], ['red']]]]
         ]
       ],
       'a selector with quoted attribute': [
         'a[data-kind=__ESCAPED_FREE_TEXT_CLEAN_CSS0__]{color:red}',
         [
-          ['selector', [['a[data-kind=__ESCAPED_FREE_TEXT_CLEAN_CSS0__]']], [['color:red']]]
+          ['selector', [['a[data-kind=__ESCAPED_FREE_TEXT_CLEAN_CSS0__]']], [[['color'], ['red']]]]
         ]
       ],
       'a selector with escaped quote': [
@@ -100,14 +118,14 @@ vows.describe(Tokenizer)
       'a double selector': [
         'a,\n\ndiv.class > p {color:red}',
         [
-          ['selector', [['a'], ['\n\ndiv.class > p ']], [['color:red']]]
+          ['selector', [['a'], ['div.class > p']], [[['color'], ['red']]]]
         ]
       ],
       'two selectors': [
         'a{color:red}div{color:blue}',
         [
-          ['selector', [['a']], [['color:red']]],
-          ['selector', [['div']], [['color:blue']]],
+          ['selector', [['a']], [[['color'], ['red']]]],
+          ['selector', [['div']], [[['color'], ['blue']]]],
         ]
       ],
       'two comments and a selector separated by newline': [
@@ -119,7 +137,7 @@ vows.describe(Tokenizer)
       'two properties wrapped between comments': [
         'div{__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS0__color:red__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS1__}',
         [
-          ['selector', [['div']], [['__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS0__'], ['color:red'], ['__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS1__']]]
+          ['selector', [['div']], ['__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS0__', [['color'], ['red']], '__ESCAPED_COMMENT_SPECIAL_CLEAN_CSS1__']]
         ]
       ],
       'pseudoselector after an argument one': [
@@ -138,7 +156,7 @@ vows.describe(Tokenizer)
         '@media (min-width:980px){a{color:red}}',
         [
           ['block', ['@media (min-width:980px)'], [
-            ['selector', [['a']], [['color:red']]]
+            ['selector', [['a']], [[['color'], ['red']]]]
           ]]
         ]
       ],
@@ -146,21 +164,21 @@ vows.describe(Tokenizer)
         '@media only screen and (max-width:1319px) and (min--moz-device-pixel-ratio:1.5),only screen and (max-width:1319px) and (-moz-min-device-pixel-ratio:1.5){a{color:#000}}',
         [
           ['block', ['@media only screen and (max-width:1319px) and (min--moz-device-pixel-ratio:1.5),only screen and (max-width:1319px) and (-moz-min-device-pixel-ratio:1.5)'], [
-            ['selector', [['a']], [['color:#000']]]
+            ['selector', [['a']], [[['color'], ['#000']]]]
           ]]
         ]
       ],
       'font-face': [
         '@font-face{font-family: fontName;font-size:12px}',
         [
-          ['flat-block', ['@font-face'], [['font-family:fontName'], ['font-size:12px']]]
+          ['flat-block', ['@font-face'], [[['font-family'], ['fontName']], [['font-size'], ['12px']]]]
         ]
       ],
       'charset': [
         '@charset \'utf-8\';a{color:red}',
         [
           ['at-rule', ['@charset \'utf-8\';']],
-          ['selector', [['a']], [['color:red']]]
+          ['selector', [['a']], [[['color'], ['red']]]]
         ]
       ],
       'charset after a line break': [
@@ -180,13 +198,95 @@ vows.describe(Tokenizer)
         [
           [
             'selector',
-            [[ 'a' ]],
-            [['border:var(--width) var(--style) var(--color)']]
+            [['a']],
+            [[['border'], ['var(--width)'], ['var(--style)'], ['var(--color)']]]
+          ]
+        ]
+      ],
+      '! important': [
+        'a{color:red! important}',
+        [
+          [
+            'selector',
+            [['a']],
+            [[['color'], ['red!important']]]
+          ]
+        ]
+      ],
+      ' !important': [
+        'a{color:red !important}',
+        [
+          [
+            'selector',
+            [['a']],
+            [[['color'], ['red!important']]]
+          ]
+        ]
+      ],
+      ' ! important': [
+        'a{color:red ! important}',
+        [
+          [
+            'selector',
+            [['a']],
+            [[['color'], ['red!important']]]
           ]
         ]
       ]
     })
   )
+  .addBatch(
+    tokenizerContext('multiple values', {
+      'comma - no spaces': [
+        'a{background:no-repeat,no-repeat}',
+        [
+          ['selector', [['a']], [[['background'], ['no-repeat'], [','], ['no-repeat']]]]
+        ]
+      ],
+      'comma - one spaces': [
+        'a{background:no-repeat, no-repeat}',
+        [
+          ['selector', [['a']], [[['background'], ['no-repeat'], [','], ['no-repeat']]]]
+        ]
+      ],
+      'comma - two spaces': [
+        'a{background:no-repeat , no-repeat}',
+        [
+          ['selector', [['a']], [[['background'], ['no-repeat'], [','], ['no-repeat']]]]
+        ]
+      ],
+      'comma - inside function': [
+        'a{background:rgba(0,0,0,0)}',
+        [
+          ['selector', [['a']], [[['background'], ['rgba(0,0,0,0)']]]]
+        ]
+      ],
+      'forward slash - no spaces': [
+        'a{border-radius:5px/4px}',
+        [
+          ['selector', [['a']], [[['border-radius'], ['5px'], ['/'], ['4px']]]]
+        ]
+      ],
+      'forward slash - one spaces': [
+        'a{border-radius:5px /4px}',
+        [
+          ['selector', [['a']], [[['border-radius'], ['5px'], ['/'], ['4px']]]]
+        ]
+      ],
+      'forward slash - two spaces': [
+        'a{border-radius:5px / 4px}',
+        [
+          ['selector', [['a']], [[['border-radius'], ['5px'], ['/'], ['4px']]]]
+        ]
+      ],
+      'forward slash - inside function': [
+        'a{width:calc(5px/4px)}',
+        [
+          ['selector', [['a']], [[['width'], ['calc(5px/4px)']]]]
+        ]
+      ],
+    })
+  )
   .addBatch(
     tokenizerContext('broken', {
       'missing end brace': [
@@ -198,7 +298,7 @@ vows.describe(Tokenizer)
       'missing end brace in the middle': [
         'body{color:red;a{color:blue;}',
         [
-          ['selector', [['body']], [['color:red']]]
+          ['selector', [['body']], [[['color'], ['red']]]]
         ]
       ]
     })
index 86e031d..bbc8c91 100644 (file)
@@ -41,10 +41,10 @@ vows.describe('source-map')
       'topic': function () {
         return new CleanCSS({ sourceMap: true }).minify('/*! a */div[data-id=" abc "] { color:red; }');
       },
-      'should have 2 mappings': function(minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 2);
+      'has 3 mappings': function(minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 3);
       },
-      'should have selector mapping': function (minified) {
+      'has selector mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 8,
@@ -55,7 +55,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
       },
-      'should have body mapping': function (minified) {
+      'has name mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 29,
@@ -65,16 +65,27 @@ vows.describe('source-map')
           name: null
         };
         assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
+      },
+      'has value mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 35,
+          originalLine: 1,
+          originalColumn: 37,
+          source: '$stdin',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
       }
     },
     'module #2': {
       'topic': function () {
         return new CleanCSS({ sourceMap: true }).minify('@media screen {\n@font-face \n{ \nfont-family: test; } }');
       },
-      'should have 3 mappings': function(minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 3);
+      'has 4 mappings': function(minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 4);
       },
-      'should have @media mapping': function (minified) {
+      'has `@media` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 0,
@@ -85,7 +96,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
       },
-      'should have @font-face mapping': function (minified) {
+      'has `@font-face mapping`': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 14,
@@ -96,7 +107,18 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
       },
-      'should have font-family mapping': function (minified) {
+      'has `font-family` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 25,
+          originalLine: 4,
+          originalColumn: 0,
+          source: '$stdin',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
+      },
+      'has `test` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 25,
@@ -112,10 +134,10 @@ vows.describe('source-map')
       'topic': function () {
         return new CleanCSS({ sourceMap: true, keepBreaks: true }).minify('@media screen { a{color:red} p {color:blue} }div{color:pink}');
       },
-      'should have 7 mappings': function(minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 7);
+      'has 10 mappings': function(minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 10);
       },
-      'should have @media mapping': function (minified) {
+      'has `@media` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 0,
@@ -126,7 +148,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
       },
-      'should have _a_ mapping': function (minified) {
+      'has `a` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 14,
@@ -137,7 +159,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
       },
-      'should have _color:red_ mapping': function (minified) {
+      'has `color` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 16,
@@ -148,7 +170,18 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
       },
-      'should have _p_ mapping': function (minified) {
+      'has `red` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 22,
+          originalLine: 1,
+          originalColumn: 24,
+          source: '$stdin',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[3], mapping);
+      },
+      'has `p` mapping': function (minified) {
         var mapping = {
           generatedLine: 2,
           generatedColumn: 0,
@@ -157,9 +190,9 @@ vows.describe('source-map')
           source: '$stdin',
           name: null
         };
-        assert.deepEqual(minified.sourceMap._mappings._array[3], mapping);
+        assert.deepEqual(minified.sourceMap._mappings._array[4], mapping);
       },
-      'should have _color:blue_ mapping': function (minified) {
+      'has second `color` mapping': function (minified) {
         var mapping = {
           generatedLine: 2,
           generatedColumn: 2,
@@ -168,9 +201,20 @@ vows.describe('source-map')
           source: '$stdin',
           name: null
         };
-        assert.deepEqual(minified.sourceMap._mappings._array[4], mapping);
+        assert.deepEqual(minified.sourceMap._mappings._array[5], mapping);
       },
-      'should have _div_ mapping': function (minified) {
+      'has `blue` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 2,
+          generatedColumn: 8,
+          originalLine: 1,
+          originalColumn: 38,
+          source: '$stdin',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[6], mapping);
+      },
+      'has `div` mapping': function (minified) {
         var mapping = {
           generatedLine: 4,
           generatedColumn: 0,
@@ -179,9 +223,9 @@ vows.describe('source-map')
           source: '$stdin',
           name: null
         };
-        assert.deepEqual(minified.sourceMap._mappings._array[5], mapping);
+        assert.deepEqual(minified.sourceMap._mappings._array[7], mapping);
       },
-      'should have _color:pink_ mapping': function (minified) {
+      'has third `color` mapping': function (minified) {
         var mapping = {
           generatedLine: 4,
           generatedColumn: 4,
@@ -190,17 +234,28 @@ vows.describe('source-map')
           source: '$stdin',
           name: null
         };
-        assert.deepEqual(minified.sourceMap._mappings._array[6], mapping);
+        assert.deepEqual(minified.sourceMap._mappings._array[8], mapping);
+      },
+      'has `pink` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 4,
+          generatedColumn: 10,
+          originalLine: 1,
+          originalColumn: 55,
+          source: '$stdin',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[9], mapping);
       }
     },
     'shorthands': {
       'topic': function () {
         return new CleanCSS({ sourceMap: true }).minify('a{background:url(image.png);background-color:red}');
       },
-      'should have 3 mappings': function(minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 3);
+      'has 3 mappings': function(minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 5);
       },
-      'should have selector mapping': function (minified) {
+      'has `a` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 0,
@@ -211,7 +266,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
       },
-      'should have _background_ mapping': function (minified) {
+      'has `background` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 2,
@@ -222,7 +277,18 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
       },
-      'should have _background-color_ mapping': function (minified) {
+      'has `url(image.png)` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 13,
+          originalLine: 1,
+          originalColumn: 13,
+          source: '$stdin',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
+      },
+      'has `background-color` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 28,
@@ -231,17 +297,28 @@ vows.describe('source-map')
           source: '$stdin',
           name: null
         };
-        assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
+        assert.deepEqual(minified.sourceMap._mappings._array[3], mapping);
+      },
+      'has `red` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 45,
+          originalLine: 1,
+          originalColumn: 45,
+          source: '$stdin',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[4], mapping);
       }
     },
     'keyframes': {
       'topic': function () {
         return new CleanCSS({ sourceMap: true }).minify('@-webkit-keyframes frames {\n  0% {\n    border: 1px;\n  }\n  100% {\n    border: 3px;\n  }\n}');
       },
-      'should have 5 mappings': function(minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 5);
+      'has 7 mappings': function(minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 7);
       },
-      'should have _@keframes_ mapping': function (minified) {
+      'has `@keframes` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 0,
@@ -252,7 +329,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
       },
-      'should have _0%_ mapping': function (minified) {
+      'has `0%` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 26,
@@ -263,7 +340,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
       },
-      'should have _border:1px_ mapping': function (minified) {
+      'has `border` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 29,
@@ -274,7 +351,18 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
       },
-      'should have _100%_ mapping': function (minified) {
+      'has `1px` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 36,
+          originalLine: 3,
+          originalColumn: 12,
+          source: '$stdin',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[3], mapping);
+      },
+      'has `100%` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 40,
@@ -283,9 +371,9 @@ vows.describe('source-map')
           source: '$stdin',
           name: null
         };
-        assert.deepEqual(minified.sourceMap._mappings._array[3], mapping);
+        assert.deepEqual(minified.sourceMap._mappings._array[4], mapping);
       },
-      'should have _border:3px_ mapping': function (minified) {
+      'has second `border` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 45,
@@ -294,17 +382,28 @@ vows.describe('source-map')
           source: '$stdin',
           name: null
         };
-        assert.deepEqual(minified.sourceMap._mappings._array[4], mapping);
+        assert.deepEqual(minified.sourceMap._mappings._array[5], mapping);
+      },
+      'has `3px` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 52,
+          originalLine: 6,
+          originalColumn: 12,
+          source: '$stdin',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[6], mapping);
       }
     },
     'double comments': {
       'topic': function () {
         return new CleanCSS({ sourceMap: true }).minify('/* COMMENT 1 */\n/* COMMENT 2 */\ndiv{color:red}');
       },
-      'should have 2 mappings': function(minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 2);
+      'has 3 mappings': function(minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 3);
       },
-      'should have _div__ mapping': function (minified) {
+      'has `div`_ mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 0,
@@ -315,7 +414,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
       },
-      'should have _color:red_ mapping': function (minified) {
+      'has `color` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 4,
@@ -325,6 +424,17 @@ vows.describe('source-map')
           name: null
         };
         assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
+      },
+      'has `red` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 10,
+          originalLine: 3,
+          originalColumn: 10,
+          source: '$stdin',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
       }
     }
   })
@@ -333,10 +443,10 @@ vows.describe('source-map')
       'topic': function () {
         return new CleanCSS({ sourceMap: inputMap }).minify('div > a {\n  color: red;\n}');
       },
-      'should have 2 mappings': function (minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 2);
+      'has 3 mappings': function (minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 3);
       },
-      'should have selector mapping': function (minified) {
+      'has `div > a` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 0,
@@ -347,7 +457,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
       },
-      'should have _color:red_ mapping': function (minified) {
+      'has `color` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 6,
@@ -357,16 +467,27 @@ vows.describe('source-map')
           name: null
         };
         assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
+      },
+      'has `red` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 12,
+          originalLine: 2,
+          originalColumn: 2,
+          source: 'styles.less',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
       }
     },
     'input map from source': {
       'topic': function () {
         return new CleanCSS({ sourceMap: true }).minify('div > a {\n  color: red;\n}/*# sourceMappingURL=' + inputMapPath + ' */');
       },
-      'should have 2 mappings': function (minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 2);
+      'has 3 mappings': function (minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 3);
       },
-      'should have selector mapping': function (minified) {
+      'has `div > a` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 0,
@@ -377,7 +498,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
       },
-      'should have _color:red_ mapping': function (minified) {
+      'has `color` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 6,
@@ -387,16 +508,27 @@ vows.describe('source-map')
           name: null
         };
         assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
+      },
+      'has second `color` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 12,
+          originalLine: 2,
+          originalColumn: 2,
+          source: path.join('test', 'fixtures', 'source-maps', 'styles.less'),
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
       }
     },
     'input map from source with root': {
       'topic': function () {
         return new CleanCSS({ sourceMap: true, root: './test/fixtures' }).minify('div > a {\n  color: red;\n}/*# sourceMappingURL=source-maps/styles.css.map */');
       },
-      'should have 2 mappings': function (minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 2);
+      'has 3 mappings': function (minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 3);
       },
-      'should have selector mapping': function (minified) {
+      'has `div > a` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 0,
@@ -407,7 +539,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
       },
-      'should have _color:red_ mapping': function (minified) {
+      'has `color` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 6,
@@ -417,16 +549,27 @@ vows.describe('source-map')
           name: null
         };
         assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
+      },
+      'has `red` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 12,
+          originalLine: 2,
+          originalColumn: 2,
+          source: path.join('source-maps', 'styles.less'),
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
       }
     },
     'input map from source with target': {
       'topic': function () {
         return new CleanCSS({ sourceMap: true, target: './test' }).minify('div > a {\n  color: red;\n}/*# sourceMappingURL=' + inputMapPath + ' */');
       },
-      'should have 2 mappings': function (minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 2);
+      'has 3 mappings': function (minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 3);
       },
-      'should have selector mapping': function (minified) {
+      'has `div > a` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 0,
@@ -437,7 +580,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
       },
-      'should have _color:red_ mapping': function (minified) {
+      'has `color` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 6,
@@ -447,16 +590,27 @@ vows.describe('source-map')
           name: null
         };
         assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
+      },
+      'has `red` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 12,
+          originalLine: 2,
+          originalColumn: 2,
+          source: path.join('fixtures', 'source-maps', 'styles.less'),
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
       }
     },
     'complex input map': {
       'topic': function () {
         return new CleanCSS({ sourceMap: true, root: path.dirname(inputMapPath) }).minify('@import url(import.css);');
       },
-      'should have 4 mappings': function (minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 4);
+      'has 6 mappings': function (minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 6);
       },
-      'should have first selector mapping': function (minified) {
+      'has `div` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 0,
@@ -467,7 +621,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
       },
-      'should have _color:red_ mapping': function (minified) {
+      'has `color` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 4,
@@ -478,7 +632,18 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
       },
-      'should have second selector mapping': function (minified) {
+      'has `red` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 10,
+          originalLine: 2,
+          originalColumn: 2,
+          source: 'some.less',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
+      },
+      'has `div > a` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 14,
@@ -487,9 +652,9 @@ vows.describe('source-map')
           source: 'styles.less',
           name: null
         };
-        assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
+        assert.deepEqual(minified.sourceMap._mappings._array[3], mapping);
       },
-      'should have _color:blue_ mapping': function (minified) {
+      'has second `color` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 20,
@@ -498,59 +663,70 @@ vows.describe('source-map')
           source: 'styles.less',
           name: null
         };
-        assert.deepEqual(minified.sourceMap._mappings._array[3], mapping);
+        assert.deepEqual(minified.sourceMap._mappings._array[4], mapping);
+      },
+      'has `blue` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 26,
+          originalLine: 2,
+          originalColumn: 2,
+          source: 'styles.less',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[5], mapping);
       }
     },
     'complex input map referenced by path': {
       'topic': function () {
         return new CleanCSS({ sourceMap: true }).minify('@import url(test/fixtures/source-maps/import.css);');
       },
-      'should have 4 mappings': function (minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 4);
+      'has 6 mappings': function (minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 6);
       }
     },
     'complex but partial input map referenced by path': {
       'topic': function () {
         return new CleanCSS({ sourceMap: true }).minify('@import url(test/fixtures/source-maps/no-map-import.css);');
       },
-      'should have 4 mappings': function (minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 4);
+      'has 6 mappings': function (minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 6);
       },
-      'should have 2 mappings to .less file': function (minified) {
+      'has 3 mappings to .less file': function (minified) {
         var fromLess = minified.sourceMap._mappings._array.filter(function (mapping) {
           return mapping.source == path.join('test', 'fixtures', 'source-maps', 'styles.less');
         });
-        assert.lengthOf(fromLess, 2);
+        assert.lengthOf(fromLess, 3);
       },
-      'should have 2 mappings to .css file': function (minified) {
+      'has 3 mappings to .css file': function (minified) {
         var fromCSS = minified.sourceMap._mappings._array.filter(function (mapping) {
           return mapping.source == path.join('test', 'fixtures', 'source-maps', 'no-map.css');
         });
-        assert.lengthOf(fromCSS, 2);
+        assert.lengthOf(fromCSS, 3);
       }
     },
     'complex input map with an existing file as target': {
       'topic': function () {
         return new CleanCSS({ sourceMap: true, target: path.join('test', 'fixtures', 'source-maps', 'styles.css') }).minify('@import url(test/fixtures/source-maps/styles.css);');
       },
-      'should have 2 mappings': function (minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 2);
+      'has 3 mappings': function (minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 3);
       },
-      'should have 2 mappings to styles.less file': function (minified) {
+      'has 3 mappings to styles.less file': function (minified) {
         var stylesSource = minified.sourceMap._mappings._array.filter(function (mapping) {
           return mapping.source == 'styles.less';
         });
-        assert.lengthOf(stylesSource, 2);
+        assert.lengthOf(stylesSource, 3);
       }
     },
     'nested once': {
       'topic': function () {
         return new CleanCSS({ sourceMap: true }).minify('@import url(test/fixtures/source-maps/nested/once.css);');
       },
-      'should have 2 mappings': function (minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 2);
+      'has 3 mappings': function (minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 3);
       },
-      'should have "section > div a" mapping': function (minified) {
+      'has `section > div a` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 0,
@@ -561,7 +737,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
       },
-      'should have "color:red" mapping': function (minified) {
+      'has `color` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 14,
@@ -571,16 +747,27 @@ vows.describe('source-map')
           name: null
         };
         assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
+      },
+      'has `red` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 20,
+          originalLine: 3,
+          originalColumn: 4,
+          source: path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'),
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
       }
     },
     'nested twice': {
       'topic': function () {
         return new CleanCSS({ sourceMap: true }).minify('@import url(test/fixtures/source-maps/nested/twice.css);');
       },
-      'should have 2 mappings': function (minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 2);
+      'has 3 mappings': function (minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 3);
       },
-      'should have "body > nav a" mapping': function (minified) {
+      'has `body > nav a` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 0,
@@ -591,7 +778,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
       },
-      'should have "color:red" mapping': function (minified) {
+      'has `color` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 11,
@@ -601,16 +788,27 @@ vows.describe('source-map')
           name: null
         };
         assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
+      },
+      'has `red` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 17,
+          originalLine: 4,
+          originalColumn: 6,
+          source: path.join('test', 'fixtures', 'source-maps', 'nested', 'twice.less'),
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
       }
     },
     'input source map with missing mutliselector input': {
       'topic': function () {
         return new CleanCSS({ sourceMap: '{"version":3,"sources":["source.css"],"names":[],"mappings":"AAAA;;;;IAII,YAAW;EACd"}' }).minify('a,\na:hover,\na:visited\n{\n    color: red;\n}');
       },
-      'should have 4 mappings': function (minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 4);
+      'has 5 mappings': function (minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 5);
       },
-      'should have "a" mapping': function (minified) {
+      'has `a` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 0,
@@ -621,7 +819,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
       },
-      'should have "a:hover" mapping': function (minified) {
+      'has `a:hover` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 2,
@@ -632,7 +830,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
       },
-      'should have "a:visited" mapping': function (minified) {
+      'has `a:visited` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 10,
@@ -643,7 +841,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
       },
-      'should have "color:red" mapping': function (minified) {
+      'has `color` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 20,
@@ -653,16 +851,27 @@ vows.describe('source-map')
           name: null
         };
         assert.deepEqual(minified.sourceMap._mappings._array[3], mapping);
+      },
+      'has `red` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 26,
+          originalLine: 5,
+          originalColumn: 4,
+          source: 'source.css',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[4], mapping);
       }
     },
     'input source map with missing mutliselector sortable input': {
       'topic': function () {
         return new CleanCSS({ sourceMap: '{"version":3,"sources":["source.css"],"names":[],"mappings":"AAAA;;;;IAII,YAAW;EACd"}' }).minify('a.button:link,\na.button:visited,\na.button:hover\n{\n    color: red;\n}');
       },
-      'should have 4 mappings': function (minified) {
-        assert.lengthOf(minified.sourceMap._mappings._array, 4);
+      'has 5 mappings': function (minified) {
+        assert.lengthOf(minified.sourceMap._mappings._array, 5);
       },
-      'should have "a.button:hover" mapping': function (minified) {
+      'has `a.button:hover` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 0,
@@ -673,7 +882,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
       },
-      'should have "a.button:link" mapping': function (minified) {
+      'has `a.button:link` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 15,
@@ -684,7 +893,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
       },
-      'should have "a.button:visited" mapping': function (minified) {
+      'has `a.button:visited` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 29,
@@ -695,7 +904,7 @@ vows.describe('source-map')
         };
         assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
       },
-      'should have "color:red" mapping': function (minified) {
+      'has `color` mapping': function (minified) {
         var mapping = {
           generatedLine: 1,
           generatedColumn: 46,
@@ -705,6 +914,17 @@ vows.describe('source-map')
           name: null
         };
         assert.deepEqual(minified.sourceMap._mappings._array[3], mapping);
+      },
+      'has `red` mapping': function (minified) {
+        var mapping = {
+          generatedLine: 1,
+          generatedColumn: 52,
+          originalLine: 5,
+          originalColumn: 4,
+          source: 'source.css',
+          name: null
+        };
+        assert.deepEqual(minified.sourceMap._mappings._array[4], mapping);
       }
     }
   })
@@ -892,10 +1112,10 @@ vows.describe('source-map')
         'has right output': function (errors, minified) {
           assert.equal(minified.styles, 'div,section>div a{color:red}div>a{color:#00f}');
         },
-        'should have 5 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        'has 7 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 7);
         },
-        'should have "div" mapping': function (minified) {
+        'has `div` mapping': function (minified) {
           var mapping = {
             generatedLine: 1,
             generatedColumn: 0,
@@ -906,7 +1126,7 @@ vows.describe('source-map')
           };
           assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
         },
-        'should have "section > div a" mapping': function (minified) {
+        'has `section > div a` mapping': function (minified) {
           var mapping = {
             generatedLine: 1,
             generatedColumn: 4,
@@ -917,7 +1137,7 @@ vows.describe('source-map')
           };
           assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
         },
-        'should have "color: red" mapping': function (minified) {
+        'has `color` mapping': function (minified) {
           var mapping = {
             generatedLine: 1,
             generatedColumn: 18,
@@ -928,7 +1148,18 @@ vows.describe('source-map')
           };
           assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
         },
-        'should have "div > a" mapping': function (minified) {
+        'has `red` mapping': function (minified) {
+          var mapping = {
+            generatedLine: 1,
+            generatedColumn: 24,
+            originalLine: 2,
+            originalColumn: 2,
+            source: path.join('test', 'fixtures', 'source-maps', 'some.less'),
+            name: null
+          };
+          assert.deepEqual(minified.sourceMap._mappings._array[3], mapping);
+        },
+        'has `div > a` mapping': function (minified) {
           var mapping = {
             generatedLine: 1,
             generatedColumn: 28,
@@ -937,9 +1168,9 @@ vows.describe('source-map')
             source: path.join('test', 'fixtures', 'source-maps', 'styles.less'),
             name: null
           };
-          assert.deepEqual(minified.sourceMap._mappings._array[3], mapping);
+          assert.deepEqual(minified.sourceMap._mappings._array[4], mapping);
         },
-        'should have "color: blue" mapping': function (minified) {
+        'has second `color` mapping': function (minified) {
           var mapping = {
             generatedLine: 1,
             generatedColumn: 34,
@@ -948,7 +1179,18 @@ vows.describe('source-map')
             source: path.join('test', 'fixtures', 'source-maps', 'styles.less'),
             name: null
           };
-          assert.deepEqual(minified.sourceMap._mappings._array[4], mapping);
+          assert.deepEqual(minified.sourceMap._mappings._array[5], mapping);
+        },
+        'has `#00f` mapping': function (minified) {
+          var mapping = {
+            generatedLine: 1,
+            generatedColumn: 40,
+            originalLine: 2,
+            originalColumn: 2,
+            source: path.join('test', 'fixtures', 'source-maps', 'styles.less'),
+            name: null
+          };
+          assert.deepEqual(minified.sourceMap._mappings._array[6], mapping);
         }
       }
     },
@@ -970,10 +1212,10 @@ vows.describe('source-map')
             }
           });
         },
-        'should have 5 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        'has 7 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 7);
         },
-        'should have right sources': function (minified) {
+        'has right sources': function (minified) {
           var sources = [];
           minified.sourceMap._mappings._array.forEach(function (m) {
             if (sources.indexOf(m.source) === -1)
@@ -995,16 +1237,16 @@ vows.describe('source-map')
         'topic': function () {
           return new CleanCSS({ sourceMap: true }).minify('div > a {\n  color: red;\n}');
         },
-        'should have 2 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 2);
+        'has 3 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 3);
         },
-        'should have embedded sources': function (minified) {
+        'has embedded sources': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, ['$stdin']);
         },
-        'should have embedded sources content': function (minified) {
+        'has embedded sources content': function (minified) {
           assert.isUndefined(JSON.parse(minified.sourceMap.toString()).sourcesContent);
         },
-        'should have selector mapping': function (minified) {
+        'has selector mapping': function (minified) {
           var mapping = {
             generatedLine: 1,
             generatedColumn: 0,
@@ -1015,7 +1257,7 @@ vows.describe('source-map')
           };
           assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
         },
-        'should have _color:red_ mapping': function (minified) {
+        'has `color` mapping': function (minified) {
           var mapping = {
             generatedLine: 1,
             generatedColumn: 6,
@@ -1025,19 +1267,30 @@ vows.describe('source-map')
             name: null
           };
           assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
+        },
+        'has `red` mapping': function (minified) {
+          var mapping = {
+            generatedLine: 1,
+            generatedColumn: 12,
+            originalLine: 2,
+            originalColumn: 9,
+            source: '$stdin',
+            name: null
+          };
+          assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
         }
       },
       'from string - on': {
         'topic': function () {
           return new CleanCSS({ sourceMap: true, sourceMapInlineSources: true }).minify('div > a {\n  color: red;\n}');
         },
-        'should have 2 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 2);
+        'has 3 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 3);
         },
-        'should have embedded sources': function (minified) {
+        'has embedded sources': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, ['$stdin']);
         },
-        'should have embedded sources content': function (minified) {
+        'has embedded sources content': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, ['div > a {\n  color: red;\n}']);
         }
       },
@@ -1048,16 +1301,16 @@ vows.describe('source-map')
             'test/fixtures/partials/three.css'
           ]);
         },
-        'should have 4 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 4);
+        'has 6 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 6);
         },
-        'should have embedded sources': function (minified) {
+        'has embedded sources': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
             path.join('test', 'fixtures', 'partials', 'one.css'),
             path.join('test', 'fixtures', 'partials', 'three.css')
           ]);
         },
-        'should have embedded sources content': function (minified) {
+        'has embedded sources content': function (minified) {
           assert.isUndefined(JSON.parse(minified.sourceMap.toString()).sourcesContent);
         }
       },
@@ -1068,16 +1321,16 @@ vows.describe('source-map')
             'test/fixtures/partials/three.css'
           ]);
         },
-        'should have 4 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 4);
+        'has 6 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 6);
         },
-        'should have embedded sources': function (minified) {
+        'has embedded sources': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
             path.join('test', 'fixtures', 'partials', 'one.css'),
             path.join('test', 'fixtures', 'partials', 'three.css')
           ]);
         },
-        'should have embedded sources content': function (minified) {
+        'has embedded sources content': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
             '.one { color:#f00; }' + lineBreak,
             '.three {background-image: url(test/fixtures/partials/extra/down.gif);}' + lineBreak
@@ -1094,15 +1347,15 @@ vows.describe('source-map')
             'http://127.0.0.1/some.css'
           ], this.callback);
         },
-        'should have 2 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 2);
+        'has 3 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 3);
         },
-        'should have embedded sources': function (minified) {
+        'has embedded sources': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
             'http://127.0.0.1/some.css'
           ]);
         },
-        'should have embedded sources content': function (minified) {
+        'has embedded sources content': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
             'div{background:url(http://127.0.0.1/image.png)}',
           ]);
@@ -1126,17 +1379,17 @@ vows.describe('source-map')
             }
           });
         },
-        'should have 5 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        'has 7 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 7);
         },
-        'should have embedded sources': function (minified) {
+        'has embedded sources': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
             'test/fixtures/source-maps/some.css',
             'test/fixtures/source-maps/nested/once.css',
             'test/fixtures/source-maps/styles.css'
           ]);
         },
-        'should have embedded sources content': function (minified) {
+        'has embedded sources content': function (minified) {
           assert.isUndefined(JSON.parse(minified.sourceMap.toString()).sourcesContent);
         }
       },
@@ -1154,17 +1407,17 @@ vows.describe('source-map')
             }
           });
         },
-        'should have 5 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        'has 7 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 7);
         },
-        'should have embedded sources': function (minified) {
+        'has embedded sources': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
             'test/fixtures/source-maps/some.css',
             'test/fixtures/source-maps/nested/once.css',
             'test/fixtures/source-maps/styles.css'
           ]);
         },
-        'should have embedded sources content': function (minified) {
+        'has embedded sources content': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
             'div {\n  color: red;\n}',
             'section > div a {\n  color: red;\n}',
@@ -1183,16 +1436,16 @@ vows.describe('source-map')
             sourceMapInlineSources: true
           }).minify('div > a {\n  color: red;\n}');
         },
-        'should have 2 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 2);
+        'has 3 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 3);
         },
-        'should have embedded sources': function (minified) {
+        'has embedded sources': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, ['styles.less']);
         },
-        'should have embedded sources content': function (minified) {
+        'has embedded sources content': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, ['div > a {\n  color: blue;\n}\n']);
         },
-        'should have selector mapping': function (minified) {
+        'has selector mapping': function (minified) {
           var mapping = {
             generatedLine: 1,
             generatedColumn: 0,
@@ -1203,7 +1456,7 @@ vows.describe('source-map')
           };
           assert.deepEqual(minified.sourceMap._mappings._array[0], mapping);
         },
-        'should have _color:red_ mapping': function (minified) {
+        'has `color` mapping': function (minified) {
           var mapping = {
             generatedLine: 1,
             generatedColumn: 6,
@@ -1213,6 +1466,17 @@ vows.describe('source-map')
             name: null
           };
           assert.deepEqual(minified.sourceMap._mappings._array[1], mapping);
+        },
+        'has `red` mapping': function (minified) {
+          var mapping = {
+            generatedLine: 1,
+            generatedColumn: 12,
+            originalLine: 2,
+            originalColumn: 2,
+            source: 'styles.less',
+            name: null
+          };
+          assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
         }
       },
       'multiple': {
@@ -1232,17 +1496,17 @@ vows.describe('source-map')
             }
           });
         },
-        'should have 5 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        'has 7 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 7);
         },
-        'should have embedded sources': function (minified) {
+        'has embedded sources': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
             path.join('test', 'fixtures', 'source-maps', 'some.less'),
             path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'),
             path.join('test', 'fixtures', 'source-maps', 'styles.less')
           ]);
         },
-        'should have embedded sources content': function (minified) {
+        'has embedded sources content': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
             'div {\n  color: red;\n}\n',
             'section {\n  > div a {\n    color:red;\n  }\n}\n',
@@ -1267,17 +1531,17 @@ vows.describe('source-map')
             }
           });
         },
-        'should have 5 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        'has 7 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 7);
         },
-        'should have embedded sources': function (minified) {
+        'has embedded sources': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
             path.join('fixtures', 'source-maps', 'some.less'),
             path.join('fixtures', 'source-maps', 'nested', 'once.less'),
             path.join('fixtures', 'source-maps', 'styles.less')
           ]);
         },
-        'should have embedded sources content': function (minified) {
+        'has embedded sources content': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
             'div {\n  color: red;\n}\n',
             'section {\n  > div a {\n    color:red;\n  }\n}\n',
@@ -1302,17 +1566,17 @@ vows.describe('source-map')
             }
           });
         },
-        'should have 5 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        'has 7 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 7);
         },
-        'should have embedded sources': function (minified) {
+        'has embedded sources': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
             path.join('test', 'fixtures', 'source-maps', 'some.less'),
             path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'),
             path.join('test', 'fixtures', 'source-maps', 'styles.less')
           ]);
         },
-        'should have embedded sources content': function (minified) {
+        'has embedded sources content': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
             'div {\n  color: red;\n}\n',
             'section {\n  > div a {\n    color:red;\n  }\n}\n',
@@ -1337,17 +1601,17 @@ vows.describe('source-map')
             }
           });
         },
-        'should have 5 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        'has 7 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 7);
         },
-        'should have embedded sources': function (minified) {
+        'has embedded sources': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
             path.join('test', 'fixtures', 'source-maps', 'some.less'),
             path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'),
             path.join('test', 'fixtures', 'source-maps', 'styles.less')
           ]);
         },
-        'should have embedded sources content': function (minified) {
+        'has embedded sources content': function (minified) {
           assert.isUndefined(JSON.parse(minified.sourceMap.toString()).sourcesContent);
         }
       },
@@ -1374,17 +1638,17 @@ vows.describe('source-map')
             }
           }, this.callback);
         },
-        'should have 5 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        'has 7 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 7);
         },
-        'should have embedded sources': function (minified) {
+        'has embedded sources': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
             'http://127.0.0.1/some.less',
             path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'),
             'http://127.0.0.1/styles.less'
           ]);
         },
-        'should have embedded sources content': function (minified) {
+        'has embedded sources content': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
             'div {\n  color: red;\n}\n',
             'section {' + lineBreak + '  > div a {' + lineBreak + '    color:red;' + lineBreak + '  }' + lineBreak + '}' + lineBreak,
@@ -1419,20 +1683,20 @@ vows.describe('source-map')
             }
           }, this.callback);
         },
-        'should have 5 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        'has 7 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 7);
         },
         'should warn about some.less': function (minified) {
           assert.deepEqual(minified.warnings, ['Broken original source file at "http://127.0.0.1/some.less" - 404']);
         },
-        'should have embedded sources': function (minified) {
+        'has embedded sources': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
             'http://127.0.0.1/some.less',
             path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'),
             'http://127.0.0.1/styles.less'
           ]);
         },
-        'should have embedded sources content': function (minified) {
+        'has embedded sources content': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
             null,
             'section {' + lineBreak + '  > div a {' + lineBreak + '    color:red;' + lineBreak + '  }' + lineBreak + '}' + lineBreak,
@@ -1461,8 +1725,8 @@ vows.describe('source-map')
             }
           });
         },
-        'should have 5 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 5);
+        'has 7 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 7);
         },
         'should warn about some.less and styles.less': function (minified) {
           assert.deepEqual(minified.warnings, [
@@ -1470,14 +1734,14 @@ vows.describe('source-map')
             'No callback given to `#minify` method, cannot fetch a remote file from "http://127.0.0.1/styles.less"'
           ]);
         },
-        'should have embedded sources': function (minified) {
+        'has embedded sources': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sources, [
             'http://127.0.0.1/some.less',
             path.join('test', 'fixtures', 'source-maps', 'nested', 'once.less'),
             'http://127.0.0.1/styles.less'
           ]);
         },
-        'should have embedded sources content': function (minified) {
+        'has embedded sources content': function (minified) {
           assert.deepEqual(JSON.parse(minified.sourceMap.toString()).sourcesContent, [
             null,
             'section {' + lineBreak + '  > div a {' + lineBreak + '    color:red;' + lineBreak + '  }' + lineBreak + '}' + lineBreak,
@@ -1493,10 +1757,13 @@ vows.describe('source-map')
         'topic': function () {
           return new CleanCSS({ sourceMap: true }).minify('a{color:#000}div{color:red}.one{display:block}.two{display:inline;color:red}');
         },
-        'should have 5 mappings': function (minified) {
-          assert.lengthOf(minified.sourceMap._mappings._array, 9);
+        'has right output': function (minified) {
+          assert.equal(minified.styles, 'a{color:#000}.two,div{color:red}.one{display:block}.two{display:inline}');
         },
-        'should have a merged ".two" mapping': function (minified) {
+        'has 13 mappings': function (minified) {
+          assert.lengthOf(minified.sourceMap._mappings._array, 13);
+        },
+        'has a merged `.two` mapping': function (minified) {
           var mapping = {
             generatedLine: 1,
             generatedColumn: 13,
@@ -1505,9 +1772,9 @@ vows.describe('source-map')
             source: '$stdin',
             name: null
           };
-          assert.deepEqual(minified.sourceMap._mappings._array[2], mapping);
+          assert.deepEqual(minified.sourceMap._mappings._array[3], mapping);
         },
-        'should have a merged "div" mapping': function (minified) {
+        'has a merged `div` mapping': function (minified) {
           var mapping = {
             generatedLine: 1,
             generatedColumn: 18,
@@ -1516,9 +1783,9 @@ vows.describe('source-map')
             source: '$stdin',
             name: null
           };
-          assert.deepEqual(minified.sourceMap._mappings._array[3], mapping);
+          assert.deepEqual(minified.sourceMap._mappings._array[4], mapping);
         },
-        'should have a merged "color:red" mapping': function (minified) {
+        'has a merged `color` mapping': function (minified) {
           var mapping = {
             generatedLine: 1,
             generatedColumn: 22,
@@ -1527,7 +1794,18 @@ vows.describe('source-map')
             source: '$stdin',
             name: null
           };
-          assert.deepEqual(minified.sourceMap._mappings._array[4], mapping);
+          assert.deepEqual(minified.sourceMap._mappings._array[5], mapping);
+        },
+        'has a merged `red` mapping': function (minified) {
+          var mapping = {
+            generatedLine: 1,
+            generatedColumn: 28,
+            originalLine: 1,
+            originalColumn: 72,
+            source: '$stdin',
+            name: null
+          };
+          assert.deepEqual(minified.sourceMap._mappings._array[6], mapping);
         }
       }
     }
index 88a2c79..5dd5fa7 100644 (file)
@@ -17,11 +17,13 @@ vows.describe(Splitter)
     'comma separated - level 2': split('linear-gradient(0,#fff,rgba(0,0,0)),red', ['linear-gradient(0,#fff,rgba(0,0,0))', 'red'], ','),
     'space separated - level 0': split('#000 #fff #0f0', ['#000', '#fff', '#0f0'], ' '),
     'space separated - level 1': split('rgb(0, 0, 0) #fff', ['rgb(0, 0, 0)', '#fff'], ' '),
-    'space separated - level 2': split('linear-gradient(0, #fff, rgba(0, 0, 0)) red', ['linear-gradient(0, #fff, rgba(0, 0, 0))', 'red'], ' ')
+    'space separated - level 2': split('linear-gradient(0, #fff, rgba(0, 0, 0)) red', ['linear-gradient(0, #fff, rgba(0, 0, 0))', 'red'], ' '),
+    'with regex': split('no-repeat,0/0', ['no-repeat', '0', '0'], /[ ,\/]/)
   })
   .addBatch({
     'leading space and quote with separator': split(' "Font"', [' "Font"'], ' ', true),
     'comma separated - level 2 - with separator': split('linear-gradient(0,#fff,rgba(0,0,0)),red', ['linear-gradient(0,#fff,rgba(0,0,0)),', 'red'], ',', true),
-    'space separated - level 2 - with separator': split('linear-gradient(0, #fff, rgba(0, 0, 0)) red', ['linear-gradient(0, #fff, rgba(0, 0, 0)) ', 'red'], ' ', true)
+    'space separated - level 2 - with separator': split('linear-gradient(0, #fff, rgba(0, 0, 0)) red', ['linear-gradient(0, #fff, rgba(0, 0, 0)) ', 'red'], ' ', true),
+    'with regex': split('no-repeat,0/0', ['no-repeat,', '0/', '0'], /[ ,\/]/, true)
   })
   .export(module);