Fixes #861 - new `transition` property optimizer.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Fri, 16 Jun 2017 17:00:42 +0000 (19:00 +0200)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Fri, 16 Jun 2017 17:07:06 +0000 (19:07 +0200)
Why:

* To collapse multiple `transition-*` properties into a shorthand
  `transition` property;
* to merge `transition-*` properties into shorthand `transition`;
* to get rid of default values in shorthands.

History.md
README.md
lib/optimizer/level-2/break-up.js
lib/optimizer/level-2/can-override.js
lib/optimizer/level-2/compactable.js
lib/optimizer/level-2/properties/override-properties.js
lib/optimizer/validator.js
test/fixtures/bootstrap-min.css
test/optimizer/level-2/break-up-test.js
test/optimizer/level-2/properties/merge-into-shorthands-test.js
test/optimizer/level-2/properties/override-properties-test.js

index a51e4ea..42b91da 100644 (file)
@@ -2,6 +2,7 @@
 ==================
 
 * Adds `process` method for compatibility with optimize-css-assets-webpack-plugin.
+* Fixed issue [#861](https://github.com/jakubpawlowicz/clean-css/issues/861) - new `transition` property optimizer.
 
 [4.1.4 / 2017-06-14](https://github.com/jakubpawlowicz/clean-css/compare/v4.1.3...v4.1.4)
 ==================
index ce7f6ac..802f82c 100644 (file)
--- a/README.md
+++ b/README.md
@@ -118,6 +118,7 @@ clean-css 4.1 introduces the following changes / features:
 clean-css 4.2 will introduce the following changes / features:
 
 * Adds `process` method for compatibility with optimize-css-assets-webpack-plugin;
+* new `transition` property optimizer;
 
 ## Constructor options
 
index b48ccf2..40b0c76 100644 (file)
@@ -105,7 +105,7 @@ function animation(property, compactable, validator) {
     } else if (validator.isTime(value[1]) && !delaySet) {
       delay.value = [value];
       delaySet = true;
-    } else if ((validator.isGlobal(value[1]) || validator.isAnimationTimingFunction(value[1])) && !timingSet) {
+    } else if ((validator.isGlobal(value[1]) || validator.isTimingFunction(value[1])) && !timingSet) {
       timing.value = [value];
       timingSet = true;
     } else if ((validator.isAnimationIterationCountKeyword(value[1]) || validator.isPositiveNumber(value[1])) && !iterationSet) {
@@ -489,6 +489,53 @@ function listStyle(property, compactable, validator) {
   return components;
 }
 
+function transition(property, compactable, validator) {
+  var prop = _wrapDefault(property.name + '-property', property, compactable);
+  var duration = _wrapDefault(property.name + '-duration', property, compactable);
+  var timing = _wrapDefault(property.name + '-timing-function', property, compactable);
+  var delay = _wrapDefault(property.name + '-delay', property, compactable);
+  var components = [prop, duration, timing, delay];
+  var values = property.value;
+  var value;
+  var durationSet = false;
+  var delaySet = false;
+  var propSet = false;
+  var timingSet = false;
+  var i;
+  var l;
+
+  if (property.value.length == 1 && property.value[0][1] == 'inherit') {
+    prop.value = duration.value = timing.value = delay.value = property.value;
+    return components;
+  }
+
+  if (values.length > 1 && _anyIsInherit(values)) {
+    throw new InvalidPropertyError('Invalid animation values at ' + formatPosition(values[0][2][0]) + '. Ignoring.');
+  }
+
+  for (i = 0, l = values.length; i < l; i++) {
+    value = values[i];
+
+    if (validator.isTime(value[1]) && !durationSet) {
+      duration.value = [value];
+      durationSet = true;
+    } else if (validator.isTime(value[1]) && !delaySet) {
+      delay.value = [value];
+      delaySet = true;
+    } else if ((validator.isGlobal(value[1]) || validator.isTimingFunction(value[1])) && !timingSet) {
+      timing.value = [value];
+      timingSet = true;
+    } else if (validator.isIdentifier(value[1]) && !propSet) {
+      prop.value = [value];
+      propSet = true;
+    } else {
+      throw new InvalidPropertyError('Invalid animation value at ' + formatPosition(value[2][0]) + '. Ignoring.');
+    }
+  }
+
+  return components;
+}
+
 function widthStyleColor(property, compactable, validator) {
   var descriptor = compactable[property.name];
   var components = [
@@ -558,5 +605,6 @@ module.exports = {
   fourValues: fourValues,
   listStyle: listStyle,
   multiplex: multiplex,
-  outline: widthStyleColor
+  outline: widthStyleColor,
+  transition: transition
 };
index 10d3bba..ceac482 100644 (file)
@@ -20,16 +20,6 @@ function animationName(validator, value1, value2) {
   return validator.isAnimationNameKeyword(value2) || validator.isIdentifier(value2);
 }
 
-function animationTimingFunction(validator, value1, value2) {
-  if (!understandable(validator, value1, value2, 0, true) && !(validator.isAnimationTimingFunction(value2) || validator.isGlobal(value2))) {
-    return false;
-  } else if (validator.isVariable(value1) && validator.isVariable(value2)) {
-    return true;
-  }
-
-  return validator.isAnimationTimingFunction(value2) || validator.isGlobal(value2);
-}
-
 function areSameFunction(validator, value1, value2) {
   if (!validator.isFunction(value1) || !validator.isFunction(value2)) {
     return false;
@@ -129,14 +119,22 @@ function keywordWithGlobal(propertyName) {
   };
 }
 
+function propertyName(validator, value1, value2) {
+  if (!understandable(validator, value1, value2, 0, true) && !validator.isIdentifier(value2)) {
+    return false;
+  } else if (validator.isVariable(value1) && validator.isVariable(value2)) {
+    return true;
+  }
+
+  return validator.isIdentifier(value2);
+}
+
 function sameFunctionOrValue(validator, value1, value2) {
   return areSameFunction(validator, value1, value2) ?
     true :
     value1 === value2;
 }
 
-
-
 function textShadow(validator, value1, value2) {
   if (!understandable(validator, value1, value2, 0, true) && !(validator.isUnit(value2) || validator.isColor(value2) || validator.isGlobal(value2))) {
     return false;
@@ -165,6 +163,16 @@ function time(validator, value1, value2) {
   return sameFunctionOrValue(validator, value1, value2);
 }
 
+function timingFunction(validator, value1, value2) {
+  if (!understandable(validator, value1, value2, 0, true) && !(validator.isTimingFunction(value2) || validator.isGlobal(value2))) {
+    return false;
+  } else if (validator.isVariable(value1) && validator.isVariable(value2)) {
+    return true;
+  }
+
+  return validator.isTimingFunction(value2) || validator.isGlobal(value2);
+}
+
 function unit(validator, value1, value2) {
   if (!understandable(validator, value1, value2, 0, true) && !validator.isUnit(value2)) {
     return false;
@@ -206,7 +214,9 @@ module.exports = {
     color: color,
     components: components,
     image: image,
+    propertyName: propertyName,
     time: time,
+    timingFunction: timingFunction,
     unit: unit
   },
   property: {
@@ -215,7 +225,6 @@ module.exports = {
     animationIterationCount: animationIterationCount,
     animationName: animationName,
     animationPlayState: keywordWithGlobal('animation-play-state'),
-    animationTimingFunction: animationTimingFunction,
     backgroundAttachment: keyword('background-attachment'),
     backgroundClip: keywordWithGlobal('background-clip'),
     backgroundOrigin: keyword('background-origin'),
index 97e7e2a..81c3f17 100644 (file)
@@ -38,7 +38,7 @@ var compactable = {
   'animation': {
     canOverride: canOverride.generic.components([
       canOverride.generic.time,
-      canOverride.property.animationTimingFunction,
+      canOverride.generic.timingFunction,
       canOverride.generic.time,
       canOverride.property.animationIterationCount,
       canOverride.property.animationDirection,
@@ -158,7 +158,7 @@ var compactable = {
     ]
   },
   'animation-timing-function': {
-    canOverride: canOverride.property.animationTimingFunction,
+    canOverride: canOverride.generic.timingFunction,
     componentOf: [
       'animation'
     ],
@@ -917,6 +917,82 @@ var compactable = {
       '-webkit-'
     ]
   },
+  'transition': {
+    breakUp: breakUp.multiplex(breakUp.transition),
+    canOverride: canOverride.generic.components([
+      canOverride.property.transitionProperty,
+      canOverride.generic.time,
+      canOverride.generic.timingFunction,
+      canOverride.generic.time
+    ]),
+    components: [
+      'transition-property',
+      'transition-duration',
+      'transition-timing-function',
+      'transition-delay'
+    ],
+    defaultValue: 'none',
+    restore: restore.multiplex(restore.withoutDefaults),
+    shorthand: true,
+    vendorPrefixes: [
+      '-moz-',
+      '-o-',
+      '-webkit-'
+    ]
+  },
+  'transition-delay': {
+    canOverride: canOverride.generic.time,
+    componentOf: [
+      'transition'
+    ],
+    defaultValue: '0s',
+    intoMultiplexMode: 'real',
+    vendorPrefixes: [
+      '-moz-',
+      '-o-',
+      '-webkit-'
+    ]
+  },
+  'transition-duration': {
+    canOverride: canOverride.generic.time,
+    componentOf: [
+      'transition'
+    ],
+    defaultValue: '0s',
+    intoMultiplexMode: 'real',
+    vendorPrefixes: [
+      '-moz-',
+      '-o-',
+      '-webkit-'
+    ]
+  },
+  'transition-property': {
+    canOverride: canOverride.generic.propertyName,
+    componentOf: [
+      'transition'
+    ],
+    defaultValue: 'all',
+    intoMultiplexMode: 'placeholder',
+    placeholderValue: '_', // it's a short value that won't match any property and still be a valid `transition-property`
+    vendorPrefixes: [
+      '-moz-',
+      '-o-',
+      '-webkit-'
+    ]
+  },
+  'transition-timing-function': {
+    canOverride: canOverride.generic.timingFunction,
+    componentOf: [
+      'transition'
+    ],
+    defaultValue: 'ease',
+    intoMultiplexMode: 'real',
+    vendorPrefixes: [
+      '-moz-',
+      '-o-',
+      '-webkit-'
+    ]
+  },
   'vertical-align': {
     canOverride: canOverride.property.verticalAlign,
     defaultValue: 'baseline'
index 3749720..2c014ea 100644 (file)
@@ -95,10 +95,11 @@ function turnShorthandValueIntoMultiplex(property, size) {
 }
 
 function turnLonghandValueIntoMultiplex(property, size) {
-  var withRealValue = compactable[property.name].intoMultiplexMode == 'real';
-  var withValue = withRealValue ?
+  var descriptor = compactable[property.name];
+  var withRealValue = descriptor.intoMultiplexMode == 'real';
+  var withValue = descriptor.intoMultiplexMode == 'real' ?
     property.value.slice(0) :
-    compactable[property.name].defaultValue;
+    (descriptor.intoMultiplexMode == 'placeholder' ? descriptor.placeholderValue : descriptor.defaultValue);
   var i = multiplexSize(property);
   var j;
   var m = withValue.length;
index cfccd0f..b393707 100644 (file)
@@ -3,7 +3,6 @@ var functionVendorRegexStr = '\\-(\\-|[A-Z]|[0-9])+\\(.*?\\)';
 var variableRegexStr = 'var\\(\\-\\-[^\\)]+\\)';
 var functionAnyRegexStr = '(' + variableRegexStr + '|' + functionNoVendorRegexStr + '|' + functionVendorRegexStr + ')';
 
-var animationTimingFunctionRegex = /^(cubic\-bezier|steps)\([^\)]+\)$/;
 var calcRegex = new RegExp('^(\\-moz\\-|\\-webkit\\-)?calc\\([^\\)]+\\)$', 'i');
 var functionAnyRegex = new RegExp('^' + functionAnyRegexStr + '$', 'i');
 var hslColorRegex = /^hsl\(\s*[\-\.\d]+\s*,\s*[\.\d]+%\s*,\s*[\.\d]+%\s*\)|hsla\(\s*[\-\.\d]+\s*,\s*[\.\d]+%\s*,\s*[\.\d]+%\s*,\s*[\.\d]+\s*\)$/;
@@ -14,6 +13,7 @@ var prefixRegex = /^-([a-z0-9]|-)*$/i;
 var rgbColorRegex = /^rgb\(\s*[\d]{1,3}\s*,\s*[\d]{1,3}\s*,\s*[\d]{1,3}\s*\)|rgba\(\s*[\d]{1,3}\s*,\s*[\d]{1,3}\s*,\s*[\d]{1,3}\s*,\s*[\.\d]+\s*\)$/;
 var shortHexColorRegex = /^#[0-9a-f]{3}$/i;
 var timeRegex = new RegExp('^(\\-?\\+?\\.?\\d+\\.?\\d*(s|ms))$');
+var timingFunctionRegex = /^(cubic\-bezier|steps)\([^\)]+\)$/;
 var urlRegex = /^url\([\s\S]+\)$/i;
 var variableRegex = new RegExp('^' + variableRegexStr + '$', 'i');
 
@@ -36,6 +36,15 @@ var Keywords = {
     'ridge',
     'solid'
   ],
+  '*-timing-function': [
+    'ease',
+    'ease-in',
+    'ease-in-out',
+    'ease-out',
+    'linear',
+    'step-end',
+    'step-start'
+  ],
   'animation-direction': [
     'alternate',
     'alternate-reverse',
@@ -58,15 +67,6 @@ var Keywords = {
     'paused',
     'running'
   ],
-  'animation-timing-function': [
-    'ease',
-    'ease-in',
-    'ease-in-out',
-    'ease-out',
-    'linear',
-    'step-end',
-    'step-start'
-  ],
   'background-attachment': [
     'fixed',
     'inherit',
@@ -337,14 +337,6 @@ var Units = [
   'vw'
 ];
 
-function isAnimationTimingFunction() {
-  var isTimingFunctionKeyword = isKeyword('animation-timing-function');
-
-  return function (value) {
-    return isTimingFunctionKeyword(value) || animationTimingFunctionRegex.test(value);
-  };
-}
-
 function isColor(value) {
   return value != 'auto' &&
     (
@@ -418,6 +410,14 @@ function isTime(value) {
   return timeRegex.test(value);
 }
 
+function isTimingFunction() {
+  var isTimingFunctionKeyword = isKeyword('*-timing-function');
+
+  return function (value) {
+    return isTimingFunctionKeyword(value) || timingFunctionRegex.test(value);
+  };
+}
+
 function isUnit(compatibleCssUnitRegex, value) {
   return compatibleCssUnitRegex.test(value);
 }
@@ -446,7 +446,7 @@ function validator(compatibility) {
     isAnimationIterationCountKeyword: isKeyword('animation-iteration-count'),
     isAnimationNameKeyword: isKeyword('animation-name'),
     isAnimationPlayStateKeyword: isKeyword('animation-play-state'),
-    isAnimationTimingFunction: isAnimationTimingFunction(),
+    isTimingFunction: isTimingFunction(),
     isBackgroundAttachmentKeyword: isKeyword('background-attachment'),
     isBackgroundClipKeyword: isKeyword('background-clip'),
     isBackgroundOriginKeyword: isKeyword('background-origin'),
index 40d233a..c23e267 100644 (file)
@@ -330,7 +330,7 @@ figure{margin:0}
 .carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}
 .img-thumbnail,.list-inline>li,label{display:inline-block}
 .img-rounded{border-radius:6px}
-.img-thumbnail{max-width:100%;height:auto;padding:4px;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}
+.img-thumbnail{max-width:100%;height:auto;padding:4px;border:1px solid #ddd;border-radius:4px;-webkit-transition:.2s ease-in-out;-o-transition:.2s ease-in-out;transition:.2s ease-in-out}
 .img-circle{border-radius:50%}
 hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}
 .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;clip:rect(0,0,0,0);border:0}
@@ -686,7 +686,7 @@ input[type=range]{display:block;width:100%}
 select[multiple],select[size]{height:auto}
 input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}
 output{padding-top:7px}
-.form-control{width:100%;height:34px;padding:6px 12px;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}
+.form-control{width:100%;height:34px;padding:6px 12px;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;-o-transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}
 .form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}
 .form-control::-moz-placeholder{color:#999;opacity:1}
 .form-control:-ms-input-placeholder{color:#999}
@@ -1145,7 +1145,7 @@ from{background-position:40px 0}
 to{background-position:0 0}
 }
 .progress{height:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}
-.progress-bar{float:left;width:0;height:100%;line-height:20px;color:#fff;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}
+.progress-bar{float:left;width:0;height:100%;line-height:20px;color:#fff;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s;-o-transition:width .6s;transition:width .6s}
 .progress-bar-striped,.progress-striped .progress-bar{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}
 .progress-bar.active,.progress.active .progress-bar{-webkit-animation:2s linear infinite progress-bar-stripes;-o-animation:2s linear infinite progress-bar-stripes;animation:2s linear infinite progress-bar-stripes}
 .progress-bar-success{background-color:#5cb85c}
@@ -1340,7 +1340,7 @@ button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;bor
 .popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:rgba(0,0,0,.25)}
 .popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}
 .carousel-inner{width:100%;overflow:hidden}
-.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}
+.carousel-inner>.item{position:relative;display:none;-webkit-transition:left .6s ease-in-out;-o-transition:left .6s ease-in-out;transition:left .6s ease-in-out}
 .carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev,.center-block{display:block}
 @media all and (transform-3d),(-webkit-transform-3d){
 .carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000;perspective:1000}
index ef86725..69a6757 100644 (file)
@@ -71,7 +71,7 @@ vows.describe(breakUp)
           assert.deepEqual(components[7].value, [['property-value', 'slidein']]);
         }
       },
-      'all with inverted order': {
+      'all with reversed order': {
         'topic': function () {
           return _breakUp([
             [
@@ -2177,4 +2177,350 @@ vows.describe(breakUp)
       }
     }
   })
+  .addBatch({
+    'transition': {
+      'all': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'transition'],
+              ['property-value', 'all'],
+              ['property-value', '1s'],
+              ['property-value', 'ease-in'],
+              ['property-value', '2s']
+            ]
+          ]);
+        },
+        'has 4 components': function (components) {
+          assert.lengthOf(components, 4);
+        },
+        'has transition-property': function (components) {
+          assert.deepEqual(components[0].name, 'transition-property');
+          assert.deepEqual(components[0].value, [['property-value', 'all']]);
+        },
+        'has transition-duration': function (components) {
+          assert.deepEqual(components[1].name, 'transition-duration');
+          assert.deepEqual(components[1].value, [['property-value', '1s']]);
+        },
+        'has transition-timing-function': function (components) {
+          assert.deepEqual(components[2].name, 'transition-timing-function');
+          assert.deepEqual(components[2].value, [['property-value', 'ease-in']]);
+        },
+        'has transition-delay': function (components) {
+          assert.deepEqual(components[3].name, 'transition-delay');
+          assert.deepEqual(components[3].value, [['property-value', '2s']]);
+        }
+      },
+      'all vendor prefixed': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', '-moz-transition'],
+              ['property-value', 'all'],
+              ['property-value', '1s'],
+              ['property-value', 'ease-in'],
+              ['property-value', '2s']
+            ]
+          ]);
+        },
+        'has 4 components': function (components) {
+          assert.lengthOf(components, 4);
+        },
+        'has -moz-transition-property': function (components) {
+          assert.deepEqual(components[0].name, '-moz-transition-property');
+          assert.deepEqual(components[0].value, [['property-value', 'all']]);
+        },
+        'has -moz-transition-duration': function (components) {
+          assert.deepEqual(components[1].name, '-moz-transition-duration');
+          assert.deepEqual(components[1].value, [['property-value', '1s']]);
+        },
+        'has -moz-transition-timing-function': function (components) {
+          assert.deepEqual(components[2].name, '-moz-transition-timing-function');
+          assert.deepEqual(components[2].value, [['property-value', 'ease-in']]);
+        },
+        'has -moz-transition-delay': function (components) {
+          assert.deepEqual(components[3].name, '-moz-transition-delay');
+          assert.deepEqual(components[3].value, [['property-value', '2s']]);
+        }
+      },
+      'all with reversed order': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'transition'],
+              ['property-value', '2s'],
+              ['property-value', 'ease-in'],
+              ['property-value', '1s'],
+              ['property-value', 'all']
+            ]
+          ]);
+        },
+        'has transition-property': function (components) {
+          assert.deepEqual(components[0].name, 'transition-property');
+          assert.deepEqual(components[0].value, [['property-value', 'all']]);
+        },
+        'has transition-duration': function (components) {
+          assert.deepEqual(components[1].name, 'transition-duration');
+          assert.deepEqual(components[1].value, [['property-value', '2s']]);
+        },
+        'has transition-timing-function': function (components) {
+          assert.deepEqual(components[2].name, 'transition-timing-function');
+          assert.deepEqual(components[2].value, [['property-value', 'ease-in']]);
+        },
+        'has transition-delay': function (components) {
+          assert.deepEqual(components[3].name, 'transition-delay');
+          assert.deepEqual(components[3].value, [['property-value', '1s']]);
+        }
+      },
+      'some': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'transition'],
+              ['property-value', 'margin'],
+              ['property-value', '1s']
+            ]
+          ]);
+        },
+        'has transition-property': function (components) {
+          assert.deepEqual(components[0].name, 'transition-property');
+          assert.deepEqual(components[0].value, [['property-value', 'margin']]);
+        },
+        'has transition-duration': function (components) {
+          assert.deepEqual(components[1].name, 'transition-duration');
+          assert.deepEqual(components[1].value, [['property-value', '1s']]);
+        },
+        'has transition-timing-function': function (components) {
+          assert.deepEqual(components[2].name, 'transition-timing-function');
+          assert.deepEqual(components[2].value, [['property-value', 'ease']]);
+        },
+        'has transition-delay': function (components) {
+          assert.deepEqual(components[3].name, 'transition-delay');
+          assert.deepEqual(components[3].value, [['property-value', '0s']]);
+        }
+      },
+      'only property': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'transition'],
+              ['property-value', 'margin']
+            ]
+          ]);
+        },
+        'has transition-property': function (components) {
+          assert.deepEqual(components[0].name, 'transition-property');
+          assert.deepEqual(components[0].value, [['property-value', 'margin']]);
+        },
+        'has transition-duration': function (components) {
+          assert.deepEqual(components[1].name, 'transition-duration');
+          assert.deepEqual(components[1].value, [['property-value', '0s']]);
+        },
+        'has transition-timing-function': function (components) {
+          assert.deepEqual(components[2].name, 'transition-timing-function');
+          assert.deepEqual(components[2].value, [['property-value', 'ease']]);
+        },
+        'has transition-delay': function (components) {
+          assert.deepEqual(components[3].name, 'transition-delay');
+          assert.deepEqual(components[3].value, [['property-value', '0s']]);
+        }
+      },
+      'only one `time`': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'transition'],
+              ['property-value', '1s']
+            ]
+          ]);
+        },
+        'has transition-property': function (components) {
+          assert.deepEqual(components[0].name, 'transition-property');
+          assert.deepEqual(components[0].value, [['property-value', 'all']]);
+        },
+        'has transition-duration': function (components) {
+          assert.deepEqual(components[1].name, 'transition-duration');
+          assert.deepEqual(components[1].value, [['property-value', '1s']]);
+        },
+        'has transition-timing-function': function (components) {
+          assert.deepEqual(components[2].name, 'transition-timing-function');
+          assert.deepEqual(components[2].value, [['property-value', 'ease']]);
+        },
+        'has transition-delay': function (components) {
+          assert.deepEqual(components[3].name, 'transition-delay');
+          assert.deepEqual(components[3].value, [['property-value', '0s']]);
+        }
+      },
+      'only two `time`s': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'transition'],
+              ['property-value', '1s'],
+              ['property-value', '2s']
+            ]
+          ]);
+        },
+        'has transition-property': function (components) {
+          assert.deepEqual(components[0].name, 'transition-property');
+          assert.deepEqual(components[0].value, [['property-value', 'all']]);
+        },
+        'has transition-duration': function (components) {
+          assert.deepEqual(components[1].name, 'transition-duration');
+          assert.deepEqual(components[1].value, [['property-value', '1s']]);
+        },
+        'has transition-timing-function': function (components) {
+          assert.deepEqual(components[2].name, 'transition-timing-function');
+          assert.deepEqual(components[2].value, [['property-value', 'ease']]);
+        },
+        'has transition-delay': function (components) {
+          assert.deepEqual(components[3].name, 'transition-delay');
+          assert.deepEqual(components[3].value, [['property-value', '2s']]);
+        }
+      },
+      'only timing function': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'transition'],
+              ['property-value', 'ease-out']
+            ]
+          ]);
+        },
+        'has transition-property': function (components) {
+          assert.deepEqual(components[0].name, 'transition-property');
+          assert.deepEqual(components[0].value, [['property-value', 'all']]);
+        },
+        'has transition-duration': function (components) {
+          assert.deepEqual(components[1].name, 'transition-duration');
+          assert.deepEqual(components[1].value, [['property-value', '0s']]);
+        },
+        'has transition-timing-function': function (components) {
+          assert.deepEqual(components[2].name, 'transition-timing-function');
+          assert.deepEqual(components[2].value, [['property-value', 'ease-out']]);
+        },
+        'has transition-delay': function (components) {
+          assert.deepEqual(components[3].name, 'transition-delay');
+          assert.deepEqual(components[3].value, [['property-value', '0s']]);
+        }
+      },
+      '`inherit`': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'transition'],
+              ['property-value', 'inherit']
+            ]
+          ]);
+        },
+        'has transition-property': function (components) {
+          assert.deepEqual(components[0].name, 'transition-property');
+          assert.deepEqual(components[0].value, [['property-value', 'inherit']]);
+        },
+        'has transition-duration': function (components) {
+          assert.deepEqual(components[1].name, 'transition-duration');
+          assert.deepEqual(components[1].value, [['property-value', 'inherit']]);
+        },
+        'has transition-timing-function': function (components) {
+          assert.deepEqual(components[2].name, 'transition-timing-function');
+          assert.deepEqual(components[2].value, [['property-value', 'inherit']]);
+        },
+        'has transition-delay': function (components) {
+          assert.deepEqual(components[3].name, 'transition-delay');
+          assert.deepEqual(components[3].value, [['property-value', 'inherit']]);
+        }
+      },
+      'multiplex': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'transition'],
+              ['property-value', 'background-color'],
+              ['property-value', '1s'],
+              ['property-value', 'ease-in'],
+              ['property-value', '1s'],
+              ['property-value', ','],
+              ['property-value', 'opacity'],
+              ['property-value', '2s']
+            ]
+          ]);
+        },
+        'has transition-property': function (components) {
+          assert.deepEqual(components[0].name, 'transition-property');
+          assert.deepEqual(components[0].value, [['property-value', 'background-color'], ['property-value', ','], ['property-value', 'opacity']]);
+        },
+        'has transition-duration': function (components) {
+          assert.deepEqual(components[1].name, 'transition-duration');
+          assert.deepEqual(components[1].value, [['property-value', '1s'], ['property-value', ','], ['property-value', '2s']]);
+        },
+        'has transition-timing-function': function (components) {
+          assert.deepEqual(components[2].name, 'transition-timing-function');
+          assert.deepEqual(components[2].value, [['property-value', 'ease-in'], ['property-value', ','], ['property-value', 'ease']]);
+        },
+        'has transition-delay': function (components) {
+          assert.deepEqual(components[3].name, 'transition-delay');
+          assert.deepEqual(components[3].value, [['property-value', '1s'], ['property-value', ','], ['property-value', '0s']]);
+        }
+      },
+      'three `time`s': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'transition'],
+              ['property-value', '1s'],
+              ['property-value', '2s'],
+              ['property-value', '3s', [[1, 30, undefined]]]
+            ]
+          ]);
+        },
+        'has no components': function (components) {
+          assert.lengthOf(components, 0);
+        }
+      },
+      'extra value': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'transition'],
+              ['property-value', 'all'],
+              ['property-value', '1s'],
+              ['property-value', 'ease-in'],
+              ['property-value', '3s'],
+              ['property-value', 'extra', [[1, 30, undefined]]]
+            ]
+          ]);
+        },
+        'has no components': function (components) {
+          assert.lengthOf(components, 0);
+        }
+      },
+      'mixed-in inherit': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'transition'],
+              ['property-value', 'all', [[1, 30, undefined]]],
+              ['property-value', 'inherit']
+            ]
+          ]);
+        },
+        'has no components': function (components) {
+          assert.lengthOf(components, 0);
+        }
+      }
+    }
+  })
   .export(module);
index 2efa17f..3c085f9 100644 (file)
@@ -814,6 +814,30 @@ vows.describe(optimizeProperties)
           ]
         ]);
       }
-  }
+    }
+  })
+  .addBatch({
+    'transition': {
+      'topic': function () {
+        return _optimize('.block{transition-property:width;transition-duration:5s;transition-timing-function:ease-in;transition-delay:2s}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', 'transition', [
+              [1, 7, undefined],
+              [1, 33, undefined],
+              [1, 56, undefined],
+              [1, 91, undefined]
+            ]],
+            ['property-value', 'width', [[1, 27, undefined]]],
+            ['property-value', '5s', [[1, 53, undefined]]],
+            ['property-value', 'ease-in', [[1, 83, undefined]]],
+            ['property-value', '2s', [[1, 108, undefined]]]
+          ]
+        ]);
+      }
+    }
   })
   .export(module);
index 62f062c..2ff59a9 100644 (file)
@@ -36,7 +36,7 @@ function _optimize(source, compat) {
 }
 
 vows.describe(optimizeProperties)
-    .addBatch({
+  .addBatch({
     'animation shorthand and longhand': {
       'topic': function () {
         return _optimize('.block{animation:1s ease-in;animation-name:slidein}');
@@ -2429,6 +2429,151 @@ vows.describe(optimizeProperties)
       }
     }
   })
+  .addBatch({
+    'transition shorthand and longhand': {
+      'topic': function () {
+        return _optimize('.block{transition:1s ease-in;transition-property:opacity}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', 'transition', [[1, 7, undefined]]],
+            ['property-value', 'opacity', [[1, 49, undefined]]],
+            ['property-value', '1s', [[1, 18, undefined]]],
+            ['property-value', 'ease-in', [[1, 21, undefined]]]
+          ]
+        ]);
+      }
+    },
+    'transition longhand and shorthand': {
+      'topic': function () {
+        return _optimize('.block{transition-duration:2s;transition:ease-in}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', 'transition', [[1, 30, undefined]]],
+            ['property-value', 'ease-in', [[1, 41, undefined]]],
+          ]
+        ]);
+      }
+    },
+    'transition shorthand with overriddable shorthand': {
+      'topic': function () {
+        return _optimize('.block{transition:opacity 1s;transition:margin 1s ease-in}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', 'transition', [[1, 7, undefined]]],
+            ['property-value', 'margin', [[1, 40, undefined]]],
+            ['property-value', '1s', [[1, 47, undefined]]],
+            ['property-value', 'ease-in', [[1, 50, undefined]]]
+          ]
+        ]);
+      }
+    },
+    'transition shorthand and multiplex longhand': {
+      'topic': function () {
+        return _optimize('.block{transition:margin 1s;transition-timing-function:ease-in,ease-out}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', 'transition', [[1, 7, undefined]]],
+            ['property-value', 'margin', [[1, 18, undefined]]],
+            ['property-value', '1s', [[1, 25, undefined]]],
+            ['property-value', 'ease-in', [[1, 55, undefined]]],
+            ['property-value', ','],
+            ['property-value', '_'],
+            ['property-value', '1s', [[1, 25, undefined]]],
+            ['property-value', 'ease-out', [[1, 63, undefined]]]
+          ]
+        ]);
+      }
+    },
+    'transition multiplex shorthand and longhand': {
+      'topic': function () {
+        return _optimize('.block{transition:ease-in,ease-out;transition-duration:1s}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', 'transition', [[1, 7, undefined]]],
+            ['property-value', '1s', [[1, 55, undefined]]],
+            ['property-value', 'ease-in', [[1, 18, undefined]]],
+            ['property-value', ','],
+            ['property-value', '1s', [[1, 55, undefined]]],
+            ['property-value', 'ease-out', [[1, 26, undefined]]]
+          ]
+        ]);
+      }
+    },
+    'transition shorthand and multiplex longhand - too long to merge': {
+      'topic': function () {
+        return _optimize('.block{transition:2s ease-in 1s;transition-property:margin,opacity,padding}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', 'transition', [[1, 7, undefined]]],
+            ['property-value', '2s', [[1, 18, undefined]]],
+            ['property-value', 'ease-in', [[1, 21, undefined]]],
+            ['property-value', '1s', [[1, 29, undefined]]]
+          ],
+          [
+            'property',
+            ['property-name', 'transition-property', [[1, 32, undefined]]],
+            ['property-value', 'margin', [[1, 52, undefined]]],
+            ['property-value', ',', [[1, 58, undefined]]],
+            ['property-value', 'opacity', [[1, 59, undefined]]],
+            ['property-value', ',', [[1, 66, undefined]]],
+            ['property-value', 'padding', [[1, 67, undefined]]]
+          ]
+        ]);
+      }
+    },
+    'transition shorthand and inherit longhand': {
+      'topic': function () {
+        return _optimize('.block{transition:1s;transition-timing-function:inherit}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', 'transition', [[1, 7, undefined]]],
+            ['property-value', '1s', [[1, 18, undefined]]],
+          ],
+          [
+            'property',
+            ['property-name', 'transition-timing-function', [[1, 21, undefined]]],
+            ['property-value', 'inherit', [[1, 48, undefined]]]
+          ]
+        ]);
+      }
+    },
+    'vendor prefixed transition shorthand and longhand': {
+      'topic': function () {
+        return _optimize('.block{-webkit-transition:1s;-webkit-transition-timing-function:ease-in}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', '-webkit-transition', [[1, 7, undefined]]],
+            ['property-value', '1s', [[1, 26, undefined]]],
+            ['property-value', 'ease-in', [[1, 64, undefined]]]
+          ]
+        ]);
+      }
+    }
+  })
   .addBatch({
     'overriding !important by a star hack': {
       'topic': function () {