Fixes #860 - adds `animation` property optimizer.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Fri, 7 Apr 2017 06:05:06 +0000 (08:05 +0200)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Wed, 19 Apr 2017 12:09:45 +0000 (14:09 +0200)
Why:

* Just as for other transformations, this code enables merging in
  longhand properties and overriding longhand properties for `animation`
  property and its components;
* some overriding examples at https://jsfiddle.net/m77mbj3f/

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/validator.js
test/fixtures/bootstrap-min.css
test/fixtures/font-awesome-min.css
test/optimizer/level-2/break-up-test.js
test/optimizer/level-2/properties/override-properties-test.js

index 84122eb..7223f16 100644 (file)
@@ -9,6 +9,7 @@
 * Fixed issue [#893](https://github.com/jakubpawlowicz/clean-css/issues/893) - `inline: false` as alias to `inline: 'none'`.
 * Fixed issue [#890](https://github.com/jakubpawlowicz/clean-css/issues/890) - adds toggle to disable empty tokens removal.
 * Fixed issue [#886](https://github.com/jakubpawlowicz/clean-css/issues/886) - better multi pseudo class / element merging.
+* Fixed issue [#860](https://github.com/jakubpawlowicz/clean-css/issues/860) - adds `animation` property optimizer.
 * Fixed issue [#755](https://github.com/jakubpawlowicz/clean-css/issues/755) - adds custom handling of remote requests.
 * Fixed issue [#254](https://github.com/jakubpawlowicz/clean-css/issues/254) - adds `font` property optimizer.
 
index 87570de..480fd7b 100644 (file)
--- a/README.md
+++ b/README.md
@@ -108,6 +108,7 @@ Once released clean-css 4.1 will introduce the following changes / features:
 * new `font` shorthand and `font-*` longhand optimizers;
 * removal of `optimizeFont` flag in level 1 optimizations due to new `font` shorthand optimizer;
 * `skipProperties` flag in level 2 optimizations controlling which properties won't be optimized;
+* new `animation` shorthand and `animation-*` longhand optimizers;
 
 ## Constructor options
 
index ec86e03..40d5c72 100644 (file)
@@ -64,6 +64,73 @@ function _widthFilter(validator) {
   };
 }
 
+function animation(property, compactable, validator) {
+  var duration = _wrapDefault('animation-duration', property, compactable);
+  var timing = _wrapDefault('animation-timing-function', property, compactable);
+  var delay = _wrapDefault('animation-delay', property, compactable);
+  var iteration = _wrapDefault('animation-iteration-count', property, compactable);
+  var direction = _wrapDefault('animation-direction', property, compactable);
+  var fill = _wrapDefault('animation-fill-mode', property, compactable);
+  var play = _wrapDefault('animation-play-state', property, compactable);
+  var name = _wrapDefault('animation-name', property, compactable);
+  var components = [duration, timing, delay, iteration, direction, fill, play, name];
+  var values = property.value;
+  var value;
+  var durationSet = false;
+  var timingSet = false;
+  var delaySet = false;
+  var iterationSet = false;
+  var directionSet = false;
+  var fillSet = false;
+  var playSet = false;
+  var nameSet = false;
+  var i;
+  var l;
+
+  if (property.value.length == 1 && property.value[0][1] == 'inherit') {
+    duration.value = timing.value = delay.value = iteration.value = direction.value = fill.value = play.value = name.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.isAnimationTimingFunction(value[1])) && !timingSet) {
+      timing.value = [value];
+      timingSet = true;
+    } else if ((validator.isAnimationIterationCountKeyword(value[1]) || validator.isPositiveNumber(value[1])) && !iterationSet) {
+      iteration.value = [value];
+      iterationSet = true;
+    } else if (validator.isAnimationDirectionKeyword(value[1]) && !directionSet) {
+      direction.value = [value];
+      directionSet = true;
+    } else if (validator.isAnimationFillModeKeyword(value[1]) && !fillSet) {
+      fill.value = [value];
+      fillSet = true;
+    } else if (validator.isAnimationPlayStateKeyword(value[1]) && !playSet) {
+      play.value = [value];
+      playSet = true;
+    } else if ((validator.isAnimationNameKeyword(value[1]) || validator.isIdentifier(value[1])) && !nameSet) {
+      name.value = [value];
+      nameSet = true;
+    } else {
+      throw new InvalidPropertyError('Invalid animation value at ' + formatPosition(value[2][0]) + '. Ignoring.');
+    }
+  }
+
+  return components;
+}
+
 function background(property, compactable, validator) {
   var image = _wrapDefault('background-image', property, compactable);
   var position = _wrapDefault('background-position', property, compactable);
@@ -482,6 +549,7 @@ function widthStyleColor(property, compactable, validator) {
 }
 
 module.exports = {
+  animation: animation,
   background: background,
   border: widthStyleColor,
   borderRadius: borderRadius,
index c960ec5..10d3bba 100644 (file)
@@ -1,5 +1,35 @@
 var understandable = require('./properties/understandable');
 
+function animationIterationCount(validator, value1, value2) {
+  if (!understandable(validator, value1, value2, 0, true) && !(validator.isAnimationIterationCountKeyword(value2) || validator.isPositiveNumber(value2))) {
+    return false;
+  } else if (validator.isVariable(value1) && validator.isVariable(value2)) {
+    return true;
+  }
+
+  return validator.isAnimationIterationCountKeyword(value2) || validator.isPositiveNumber(value2);
+}
+
+function animationName(validator, value1, value2) {
+  if (!understandable(validator, value1, value2, 0, true) && !(validator.isAnimationNameKeyword(value2) || validator.isIdentifier(value2))) {
+    return false;
+  } else if (validator.isVariable(value1) && validator.isVariable(value2)) {
+    return true;
+  }
+
+  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;
@@ -117,6 +147,24 @@ function textShadow(validator, value1, value2) {
   return validator.isUnit(value2) || validator.isColor(value2) || validator.isGlobal(value2);
 }
 
+function time(validator, value1, value2) {
+  if (!understandable(validator, value1, value2, 0, true) && !validator.isTime(value2)) {
+    return false;
+  } else if (validator.isVariable(value1) && validator.isVariable(value2)) {
+    return true;
+  } else if (validator.isTime(value1) && !validator.isTime(value2)) {
+    return false;
+  } else if (validator.isTime(value2)) {
+    return true;
+  } else if (validator.isTime(value1)) {
+    return false;
+  } else if (validator.isFunction(value1) && !validator.isPrefixed(value1) && validator.isFunction(value2) && !validator.isPrefixed(value2)) {
+    return true;
+  }
+
+  return sameFunctionOrValue(validator, value1, value2);
+}
+
 function unit(validator, value1, value2) {
   if (!understandable(validator, value1, value2, 0, true) && !validator.isUnit(value2)) {
     return false;
@@ -158,9 +206,16 @@ module.exports = {
     color: color,
     components: components,
     image: image,
+    time: time,
     unit: unit
   },
   property: {
+    animationDirection: keywordWithGlobal('animation-direction'),
+    animationFillMode: keyword('animation-fill-mode'),
+    animationIterationCount: animationIterationCount,
+    animationName: animationName,
+    animationPlayState: keywordWithGlobal('animation-play-state'),
+    animationTimingFunction: animationTimingFunction,
     backgroundAttachment: keyword('background-attachment'),
     backgroundClip: keywordWithGlobal('background-clip'),
     backgroundOrigin: keyword('background-origin'),
index 8fb5dc7..097b6cf 100644 (file)
@@ -35,6 +35,141 @@ var override = require('../../utils/override');
 //   Puts the shorthand together from its components.
 //
 var compactable = {
+  'animation': {
+    canOverride: canOverride.generic.components([
+      canOverride.generic.time,
+      canOverride.property.animationTimingFunction,
+      canOverride.generic.time,
+      canOverride.property.animationIterationCount,
+      canOverride.property.animationDirection,
+      canOverride.property.animationFillMode,
+      canOverride.property.animationPlayState,
+      canOverride.property.animationName
+    ]),
+    components: [
+      'animation-duration',
+      'animation-timing-function',
+      'animation-delay',
+      'animation-iteration-count',
+      'animation-direction',
+      'animation-fill-mode',
+      'animation-play-state',
+      'animation-name'
+    ],
+    breakUp: breakUp.multiplex(breakUp.animation),
+    defaultValue: 'none',
+    restore: restore.multiplex(restore.withoutDefaults),
+    shorthand: true,
+    vendorPrefixes: [
+      '-moz-',
+      '-o-',
+      '-webkit-'
+    ]
+  },
+  'animation-delay': {
+    canOverride: canOverride.generic.time,
+    componentOf: [
+      'animation'
+    ],
+    defaultValue: '0s',
+    intoMultiplexMode: 'real',
+    vendorPrefixes: [
+      '-moz-',
+      '-o-',
+      '-webkit-'
+    ]
+  },
+  'animation-direction': {
+    canOverride: canOverride.property.animationDirection,
+    componentOf: [
+      'animation'
+    ],
+    defaultValue: 'normal',
+    intoMultiplexMode: 'real',
+    vendorPrefixes: [
+      '-moz-',
+      '-o-',
+      '-webkit-'
+    ]
+  },
+  'animation-duration': {
+    canOverride: canOverride.generic.time,
+    componentOf: [
+      'animation'
+    ],
+    defaultValue: '0s',
+    intoMultiplexMode: 'real',
+    vendorPrefixes: [
+      '-moz-',
+      '-o-',
+      '-webkit-'
+    ]
+  },
+  'animation-fill-mode': {
+    canOverride: canOverride.property.animationFillMode,
+    componentOf: [
+      'animation'
+    ],
+    defaultValue: 'none',
+    intoMultiplexMode: 'real',
+    vendorPrefixes: [
+      '-moz-',
+      '-o-',
+      '-webkit-'
+    ]
+  },
+  'animation-iteration-count': {
+    canOverride: canOverride.property.animationIterationCount,
+    componentOf: [
+      'animation'
+    ],
+    defaultValue: '1',
+    intoMultiplexMode: 'real',
+    vendorPrefixes: [
+      '-moz-',
+      '-o-',
+      '-webkit-'
+    ]
+  },
+  'animation-name': {
+    canOverride: canOverride.property.animationName,
+    componentOf: [
+      'animation'
+    ],
+    defaultValue: 'none',
+    intoMultiplexMode: 'real',
+    vendorPrefixes: [
+      '-moz-',
+      '-o-',
+      '-webkit-'
+    ]
+  },
+  'animation-play-state': {
+    canOverride: canOverride.property.animationPlayState,
+    componentOf: [
+      'animation'
+    ],
+    defaultValue: 'running',
+    intoMultiplexMode: 'real',
+    vendorPrefixes: [
+      '-moz-',
+      '-o-',
+      '-webkit-'
+    ]
+  },
+  'animation-timing-function': {
+    canOverride: canOverride.property.animationTimingFunction,
+    componentOf: [
+      'animation'
+    ],
+    defaultValue: 'ease',
+    intoMultiplexMode: 'real',
+    vendorPrefixes: [
+      '-moz-',
+      '-o-',
+      '-webkit-'
+    ]
+  },
   'background': {
     canOverride: canOverride.generic.components([
       canOverride.generic.image,
index a4b8b3d..cc1dc41 100644 (file)
@@ -3,14 +3,17 @@ 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*\)$/;
+var identifierRegex = /^(\-[a-z0-9_][a-z0-9\-_]*|[a-z][a-z0-9\-_]*)$/i;
 var longHexColorRegex = /^#[0-9a-f]{6}$/i;
 var namedEntityRegex = /^[a-z]+$/i;
 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 urlRegex = /^url\([\s\S]+\)$/i;
 var variableRegex = new RegExp('^' + variableRegexStr + '$', 'i');
 
@@ -33,6 +36,37 @@ var Keywords = {
     'ridge',
     'solid'
   ],
+  'animation-direction': [
+    'alternate',
+    'alternate-reverse',
+    'normal',
+    'reverse'
+  ],
+  'animation-fill-mode': [
+    'backwards',
+    'both',
+    'forwards',
+    'none'
+  ],
+  'animation-iteration-count': [
+    'infinite'
+  ],
+  'animation-name': [
+    'none'
+  ],
+  'animation-play-state': [
+    'paused',
+    'running'
+  ],
+  'animation-timing-function': [
+    'ease',
+    'ease-in',
+    'ease-in-out',
+    'ease-out',
+    'linear',
+    'step-end',
+    'step-start'
+  ],
   'background-attachment': [
     'fixed',
     'inherit',
@@ -302,6 +336,14 @@ 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' &&
     (
@@ -332,6 +374,10 @@ function isHslColor(value) {
   return hslColorRegex.test(value);
 }
 
+function isIdentifier(value) {
+  return identifierRegex.test(value);
+}
+
 function isImage(value) {
   return value == 'none' || value == 'inherit' || isUrl(value);
 }
@@ -358,10 +404,19 @@ function isPrefixed(value) {
   return prefixRegex.test(value);
 }
 
+function isPositiveNumber(value) {
+  return isNumber(value) &&
+    parseFloat(value) >= 0;
+}
+
 function isVariable(value) {
   return variableRegex.test(value);
 }
 
+function isTime(value) {
+  return timeRegex.test(value);
+}
+
 function isUnit(compatibleCssUnitRegex, value) {
   return compatibleCssUnitRegex.test(value);
 }
@@ -385,6 +440,12 @@ function validator(compatibility) {
 
   return {
     colorOpacity: compatibility.colors.opacity,
+    isAnimationDirectionKeyword: isKeyword('animation-direction'),
+    isAnimationFillModeKeyword: isKeyword('animation-fill-mode'),
+    isAnimationIterationCountKeyword: isKeyword('animation-iteration-count'),
+    isAnimationNameKeyword: isKeyword('animation-name'),
+    isAnimationPlayStateKeyword: isKeyword('animation-play-state'),
+    isAnimationTimingFunction: isAnimationTimingFunction(),
     isBackgroundAttachmentKeyword: isKeyword('background-attachment'),
     isBackgroundClipKeyword: isKeyword('background-clip'),
     isBackgroundOriginKeyword: isKeyword('background-origin'),
@@ -404,13 +465,16 @@ function validator(compatibility) {
     isGlobal: isKeyword('^'),
     isHslColor: isHslColor,
     isImage: isImage,
+    isIdentifier: isIdentifier,
     isKeyword: isKeyword,
     isLineHeightKeyword: isKeyword('line-height'),
     isListStylePositionKeyword: isKeyword('list-style-position'),
     isListStyleTypeKeyword: isKeyword('list-style-type'),
     isPrefixed: isPrefixed,
+    isPositiveNumber: isPositiveNumber,
     isRgbColor: isRgbColor,
     isStyleKeyword: isKeyword('*-style'),
+    isTime: isTime,
     isUnit: isUnit.bind(null, compatibleCssUnitRegex),
     isUrl: isUrl,
     isVariable: isVariable,
index b3e5537..40d233a 100644 (file)
@@ -1147,7 +1147,7 @@ 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-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:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}
+.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}
 .progress-striped .progress-bar-success{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)}
 .progress-striped .progress-bar-info,.progress-striped .progress-bar-warning{background-image:-webkit-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);background-image:-o-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)}
@@ -1453,4 +1453,4 @@ td.visible-print,th.visible-print{display:table-cell!important}
 @media print{
 .visible-print-inline-block{display:inline-block!important}
 .hidden-print{display:none!important}
-}
+}
\ No newline at end of file
index 09ffec7..f6b17a7 100644 (file)
@@ -51,7 +51,7 @@ ul.icons li [class*=" icon-"],ul.icons li [class^=icon-]{width:.75em}
 .btn.btn-large [class*=" icon-"].pull-left.icon-2x,.btn.btn-large [class*=" icon-"].pull-right.icon-2x,.btn.btn-large [class^=icon-].pull-left.icon-2x,.btn.btn-large [class^=icon-].pull-right.icon-2x{margin-top:.05em}
 .btn.btn-large [class*=" icon-"].pull-left.icon-2x,.btn.btn-large [class^=icon-].pull-left.icon-2x{margin-right:.2em}
 .btn.btn-large [class*=" icon-"].pull-right.icon-2x,.btn.btn-large [class^=icon-].pull-right.icon-2x{margin-left:.2em}
-.icon-spin{-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-webkit-animation:spin 2s infinite linear;animation:spin 2s infinite linear}
+.icon-spin{-moz-animation:2s linear infinite spin;-o-animation:2s linear infinite spin;-webkit-animation:2s linear infinite spin;animation:2s linear infinite spin}
 @-moz-keyframes spin{
 0%{-moz-transform:rotate(0)}
 100%{-moz-transform:rotate(359deg)}
index 2617203..e80dc26 100644 (file)
@@ -17,6 +17,367 @@ function _breakUp(properties) {
 
 vows.describe(breakUp)
   .addBatch({
+    'animation': {
+      'all': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'animation'],
+              ['property-value', '3s'],
+              ['property-value', 'ease-in'],
+              ['property-value', '1s'],
+              ['property-value', '2'],
+              ['property-value', 'reverse'],
+              ['property-value', 'both'],
+              ['property-value', 'paused'],
+              ['property-value', 'slidein']
+            ]
+          ]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has animation-duration': function (components) {
+          assert.deepEqual(components[0].name, 'animation-duration');
+          assert.deepEqual(components[0].value, [['property-value', '3s']]);
+        },
+        'has animation-timing-function': function (components) {
+          assert.deepEqual(components[1].name, 'animation-timing-function');
+          assert.deepEqual(components[1].value, [['property-value', 'ease-in']]);
+        },
+        'has animation-delay': function (components) {
+          assert.deepEqual(components[2].name, 'animation-delay');
+          assert.deepEqual(components[2].value, [['property-value', '1s']]);
+        },
+        'has animation-iteration-count': function (components) {
+          assert.deepEqual(components[3].name, 'animation-iteration-count');
+          assert.deepEqual(components[3].value, [['property-value', '2']]);
+        },
+        'has animation-direction': function (components) {
+          assert.deepEqual(components[4].name, 'animation-direction');
+          assert.deepEqual(components[4].value, [['property-value', 'reverse']]);
+        },
+        'has animation-fill-mode': function (components) {
+          assert.deepEqual(components[5].name, 'animation-fill-mode');
+          assert.deepEqual(components[5].value, [['property-value', 'both']]);
+        },
+        'has animation-play-state': function (components) {
+          assert.deepEqual(components[6].name, 'animation-play-state');
+          assert.deepEqual(components[6].value, [['property-value', 'paused']]);
+        },
+        'has animation-name': function (components) {
+          assert.deepEqual(components[7].name, 'animation-name');
+          assert.deepEqual(components[7].value, [['property-value', 'slidein']]);
+        }
+      },
+      'all with inverted order': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'animation'],
+              ['property-value', 'slidein'],
+              ['property-value', 'paused'],
+              ['property-value', 'both'],
+              ['property-value', 'reverse'],
+              ['property-value', '2'],
+              ['property-value', '1s'],
+              ['property-value', 'ease-in'],
+              ['property-value', '3s']
+            ]
+          ]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has animation-duration': function (components) {
+          assert.deepEqual(components[0].name, 'animation-duration');
+          assert.deepEqual(components[0].value, [['property-value', '1s']]);
+        },
+        'has animation-timing-function': function (components) {
+          assert.deepEqual(components[1].name, 'animation-timing-function');
+          assert.deepEqual(components[1].value, [['property-value', 'ease-in']]);
+        },
+        'has animation-delay': function (components) {
+          assert.deepEqual(components[2].name, 'animation-delay');
+          assert.deepEqual(components[2].value, [['property-value', '3s']]);
+        },
+        'has animation-iteration-count': function (components) {
+          assert.deepEqual(components[3].name, 'animation-iteration-count');
+          assert.deepEqual(components[3].value, [['property-value', '2']]);
+        },
+        'has animation-direction': function (components) {
+          assert.deepEqual(components[4].name, 'animation-direction');
+          assert.deepEqual(components[4].value, [['property-value', 'reverse']]);
+        },
+        'has animation-fill-mode': function (components) {
+          assert.deepEqual(components[5].name, 'animation-fill-mode');
+          assert.deepEqual(components[5].value, [['property-value', 'both']]);
+        },
+        'has animation-play-state': function (components) {
+          assert.deepEqual(components[6].name, 'animation-play-state');
+          assert.deepEqual(components[6].value, [['property-value', 'paused']]);
+        },
+        'has animation-name': function (components) {
+          assert.deepEqual(components[7].name, 'animation-name');
+          assert.deepEqual(components[7].value, [['property-value', 'slidein']]);
+        }
+      },
+      'some': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'animation'],
+              ['property-value', '3s'],
+              ['property-value', 'reverse'],
+              ['property-value', 'ease-in']
+            ]
+          ]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has animation-duration': function (components) {
+          assert.deepEqual(components[0].name, 'animation-duration');
+          assert.deepEqual(components[0].value, [['property-value', '3s']]);
+        },
+        'has animation-timing-function': function (components) {
+          assert.deepEqual(components[1].name, 'animation-timing-function');
+          assert.deepEqual(components[1].value, [['property-value', 'ease-in']]);
+        },
+        'has animation-delay': function (components) {
+          assert.deepEqual(components[2].name, 'animation-delay');
+          assert.deepEqual(components[2].value, [['property-value', '0s']]);
+        },
+        'has animation-iteration-count': function (components) {
+          assert.deepEqual(components[3].name, 'animation-iteration-count');
+          assert.deepEqual(components[3].value, [['property-value', '1']]);
+        },
+        'has animation-direction': function (components) {
+          assert.deepEqual(components[4].name, 'animation-direction');
+          assert.deepEqual(components[4].value, [['property-value', 'reverse']]);
+        },
+        'has animation-fill-mode': function (components) {
+          assert.deepEqual(components[5].name, 'animation-fill-mode');
+          assert.deepEqual(components[5].value, [['property-value', 'none']]);
+        },
+        'has animation-play-state': function (components) {
+          assert.deepEqual(components[6].name, 'animation-play-state');
+          assert.deepEqual(components[6].value, [['property-value', 'running']]);
+        },
+        'has animation-name': function (components) {
+          assert.deepEqual(components[7].name, 'animation-name');
+          assert.deepEqual(components[7].value, [['property-value', 'none']]);
+        }
+      },
+      'custom timing function': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'animation'],
+              ['property-value', 'cubic-bezier(0.1, 0.7, 1.0, 0.1)']
+            ]
+          ]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has animation-timing-function': function (components) {
+          assert.deepEqual(components[1].name, 'animation-timing-function');
+          assert.deepEqual(components[1].value, [['property-value', 'cubic-bezier(0.1, 0.7, 1.0, 0.1)']]);
+        }
+      },
+      'invalid timing function': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'animation'],
+              ['property-value', 'custom-bezier(0.1, 0.7, 1.0, 0.1)', [[1, 12, undefined]]]
+            ]
+          ]);
+        },
+        'has no components': function (components) {
+          assert.lengthOf(components, 0);
+        }
+      },
+      'custom animation name': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'animation'],
+              ['property-value', 'custom-animation']
+            ]
+          ]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has animation-name': function (components) {
+          assert.deepEqual(components[7].name, 'animation-name');
+          assert.deepEqual(components[7].value, [['property-value', 'custom-animation']]);
+        }
+      },
+      'three time units': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'animation'],
+              ['property-value', '1s'],
+              ['property-value', 'ease-in'],
+              ['property-value', '2s'],
+              ['property-value', '3s', [[1, 20, undefined]]]
+            ]
+          ]);
+        },
+        'has no components': function (components) {
+          assert.lengthOf(components, 0);
+        }
+      },
+      'repeated values': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'animation'],
+              ['property-value', '1s'],
+              ['property-value', 'reverse'],
+              ['property-value', 'reverse']
+            ]
+          ]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has animation-name': function (components) {
+          assert.deepEqual(components[7].name, 'animation-name');
+          assert.deepEqual(components[7].value, [['property-value', 'reverse']]);
+        }
+      },
+      'inherit': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'animation'],
+              ['property-value', 'inherit']
+            ]
+          ]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has animation-duration': function (components) {
+          assert.deepEqual(components[0].name, 'animation-duration');
+          assert.deepEqual(components[0].value, [['property-value', 'inherit']]);
+        },
+        'has animation-timing-function': function (components) {
+          assert.deepEqual(components[1].name, 'animation-timing-function');
+          assert.deepEqual(components[1].value, [['property-value', 'inherit']]);
+        },
+        'has animation-delay': function (components) {
+          assert.deepEqual(components[2].name, 'animation-delay');
+          assert.deepEqual(components[2].value, [['property-value', 'inherit']]);
+        },
+        'has animation-iteration-count': function (components) {
+          assert.deepEqual(components[3].name, 'animation-iteration-count');
+          assert.deepEqual(components[3].value, [['property-value', 'inherit']]);
+        },
+        'has animation-direction': function (components) {
+          assert.deepEqual(components[4].name, 'animation-direction');
+          assert.deepEqual(components[4].value, [['property-value', 'inherit']]);
+        },
+        'has animation-fill-mode': function (components) {
+          assert.deepEqual(components[5].name, 'animation-fill-mode');
+          assert.deepEqual(components[5].value, [['property-value', 'inherit']]);
+        },
+        'has animation-play-state': function (components) {
+          assert.deepEqual(components[6].name, 'animation-play-state');
+          assert.deepEqual(components[6].value, [['property-value', 'inherit']]);
+        },
+        'has animation-name': function (components) {
+          assert.deepEqual(components[7].name, 'animation-name');
+          assert.deepEqual(components[7].value, [['property-value', 'inherit']]);
+        }
+      },
+      'inherit mixed in': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'animation'],
+              ['property-value', '1s', [[1, 12, undefined]]],
+              ['property-value', 'inherit']
+            ]
+          ]);
+        },
+        'has no components': function (components) {
+          assert.lengthOf(components, 0);
+        }
+      },
+      'multiplex': {
+        'topic': function () {
+          return _breakUp([
+            [
+              'property',
+              ['property-name', 'animation'],
+              ['property-value', '3s'],
+              ['property-value', 'ease-in'],
+              ['property-value', '1s'],
+              ['property-value', '2'],
+              ['property-value', 'reverse'],
+              ['property-value', 'both'],
+              ['property-value', 'paused'],
+              ['property-value', 'slidein'],
+              ['property-value', ','],
+              ['property-value', '2s'],
+              ['property-value', 'ease-out'],
+              ['property-value', 'slideout']
+            ]
+          ]);
+        },
+        'has 8 components': function (components) {
+          assert.lengthOf(components, 8);
+        },
+        'has animation-duration': function (components) {
+          assert.deepEqual(components[0].name, 'animation-duration');
+          assert.deepEqual(components[0].value, [['property-value', '3s'], ['property-value', ','], ['property-value', '2s']]);
+        },
+        'has animation-timing-function': function (components) {
+          assert.deepEqual(components[1].name, 'animation-timing-function');
+          assert.deepEqual(components[1].value, [['property-value', 'ease-in'], ['property-value', ','], ['property-value', 'ease-out']]);
+        },
+        'has animation-delay': function (components) {
+          assert.deepEqual(components[2].name, 'animation-delay');
+          assert.deepEqual(components[2].value, [['property-value', '1s'], ['property-value', ','], ['property-value', '0s']]);
+        },
+        'has animation-iteration-count': function (components) {
+          assert.deepEqual(components[3].name, 'animation-iteration-count');
+          assert.deepEqual(components[3].value, [['property-value', '2'], ['property-value', ','], ['property-value', '1']]);
+        },
+        'has animation-direction': function (components) {
+          assert.deepEqual(components[4].name, 'animation-direction');
+          assert.deepEqual(components[4].value, [['property-value', 'reverse'], ['property-value', ','], ['property-value', 'normal']]);
+        },
+        'has animation-fill-mode': function (components) {
+          assert.deepEqual(components[5].name, 'animation-fill-mode');
+          assert.deepEqual(components[5].value, [['property-value', 'both'], ['property-value', ','], ['property-value', 'none']]);
+        },
+        'has animation-play-state': function (components) {
+          assert.deepEqual(components[6].name, 'animation-play-state');
+          assert.deepEqual(components[6].value, [['property-value', 'paused'], ['property-value', ','], ['property-value', 'running']]);
+        },
+        'has animation-name': function (components) {
+          assert.deepEqual(components[7].name, 'animation-name');
+          assert.deepEqual(components[7].value, [['property-value', 'slidein'], ['property-value', ','], ['property-value', 'slideout']]);
+        }
+      },
+    },
     'background': {
       'inherit': {
         'topic': function () {
index 9ea5040..4a356c8 100644 (file)
@@ -36,6 +36,137 @@ function _optimize(source, compat) {
 }
 
 vows.describe(optimizeProperties)
+    .addBatch({
+    'animation shorthand and longhand': {
+      'topic': function () {
+        return _optimize('.block{animation:1s ease-in;animation-name:slidein}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', 'animation', [[1, 7, undefined]]],
+            ['property-value', '1s', [[1, 17, undefined]]],
+            ['property-value', 'ease-in', [[1, 20, undefined]]],
+            ['property-value', 'slidein', [[1, 43, undefined]]]
+          ]
+        ]);
+      }
+    },
+    'animation longhand and shorthand': {
+      'topic': function () {
+        return _optimize('.block{animation-fill-mode:both;animation:ease-in}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', 'animation', [[1, 32, undefined]]],
+            ['property-value', 'ease-in', [[1, 42, undefined]]],
+          ]
+        ]);
+      }
+    },
+    'animation shorthand with overriddable shorthand': {
+      'topic': function () {
+        return _optimize('.block{animation:1s infinite slidein;animation:ease-in 2}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', 'animation', [[1, 7, undefined]]],
+            ['property-value', 'ease-in', [[1, 47, undefined]]],
+            ['property-value', '2', [[1, 55, undefined]]]
+          ]
+        ]);
+      }
+    },
+    'animation shorthand and multiplex longhand': {
+      'topic': function () {
+        return _optimize('.block{animation:1s infinite slidein;animation-timing-function:ease-in,ease-out}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', 'animation', [[1, 7, undefined]]],
+            ['property-value', '1s', [[1, 17, undefined]]],
+            ['property-value', 'ease-in', [[1, 63, undefined]]],
+            ['property-value', 'infinite', [[1, 20, undefined]]],
+            ['property-value', 'slidein', [[1, 29, undefined]]],
+            ['property-value', ','],
+            ['property-value', '1s', [[1, 17, undefined]]],
+            ['property-value', 'ease-out', [[1, 71, undefined]]],
+            ['property-value', 'infinite', [[1, 20, undefined]]],
+            ['property-value', 'slidein', [[1, 29, undefined]]]
+          ]
+        ]);
+      }
+    },
+    'animation multiplex shorthand and longhand': {
+      'topic': function () {
+        return _optimize('.block{animation:ease-in,ease-out;animation-duration:1s}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', 'animation', [[1, 7, undefined]]],
+            ['property-value', '1s', [[1, 53, undefined]]],
+            ['property-value', 'ease-in', [[1, 17, undefined]]],
+            ['property-value', ','],
+            ['property-value', '1s', [[1, 53, undefined]]],
+            ['property-value', 'ease-out', [[1, 25, undefined]]]
+          ]
+        ]);
+      }
+    },
+    'animation shorthand and multiplex longhand - too long to merge': {
+      'topic': function () {
+        return _optimize('.block{animation:ease-in;animation-name:longname1,longname2,longname3}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', 'animation', [[1, 7, undefined]]],
+            ['property-value', 'ease-in', [[1, 17, undefined]]],
+          ],
+          [
+            'property',
+            ['property-name', 'animation-name', [[1, 25, undefined]]],
+            ['property-value', 'longname1', [[1, 40, undefined]]],
+            ['property-value', ',', [[1, 49, undefined]]],
+            ['property-value', 'longname2', [[1, 50, undefined]]],
+            ['property-value', ',', [[1, 59, undefined]]],
+            ['property-value', 'longname3', [[1, 60, undefined]]]
+          ]
+        ]);
+      }
+    },
+    'animation shorthand and inherit longhand': {
+      'topic': function () {
+        return _optimize('.block{animation:1s infinite slidein;animation-timing-function:inherit}');
+      },
+      'into': function (properties) {
+        assert.deepEqual(properties, [
+          [
+            'property',
+            ['property-name', 'animation', [[1, 7, undefined]]],
+            ['property-value', '1s', [[1, 17, undefined]]],
+            ['property-value', 'infinite', [[1, 20, undefined]]],
+            ['property-value', 'slidein', [[1, 29, undefined]]]
+          ],
+          [
+            'property',
+            ['property-name', 'animation-timing-function', [[1, 37, undefined]]],
+            ['property-value', 'inherit', [[1, 63, undefined]]]
+          ]
+        ]);
+      }
+    }
+  })
   .addBatch({
     'longhand then longhand - background colors as functions': {
       'topic': function () {