Adds more granular control over compatibility settings.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Tue, 14 Oct 2014 07:09:07 +0000 (08:09 +0100)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Wed, 15 Oct 2014 21:10:56 +0000 (22:10 +0100)
* Manages compatibility options as a hash of options.
* Handles fallback to previous compatibility options.

13 files changed:
History.md
README.md
bin/cleancss
lib/clean.js
lib/properties/optimizer.js
lib/properties/override-compactor.js
lib/selectors/optimizers/advanced.js
lib/selectors/optimizers/simple.js
lib/utils/compatibility.js [new file with mode: 0644]
test/binary-test.js
test/selectors/optimizer-test.js
test/selectors/optimizers/simple-test.js
test/utils/compatibility-test.js [new file with mode: 0644]

index 8498c60..69c386b 100644 (file)
@@ -1,6 +1,7 @@
 [3.0.0 / 2014-xx-xx](https://github.com/jakubpawlowicz/clean-css/compare/v2.2.15...v3.0.0)
 ==================
 
+* Adds more granular control over compatibility settings.
 * Allows `target` option to be a path to a folder instead of a file.
 * Breaks 2.x compatibility for using CleanCSS as a function.
 * Reworks minification to tokenize first then minify.
index 3b7890a..875bebb 100644 (file)
--- a/README.md
+++ b/README.md
@@ -86,7 +86,7 @@ cleancss [options] source-file, [source-file, ...]
                                 reduction, etc.
 --skip-aggressive-merging       Disable properties merging based on their order
 --rounding-precision [N]        Rounds pixel values to `N` decimal places, defaults to 2
--c, --compatibility [ie7|ie8]   Force compatibility mode
+-c, --compatibility [ie7|ie8]   Force compatibility mode (see Readme for advanced examples)
 -d, --debug                     Shows debug information (minification time & compression efficiency)
 ```
 
@@ -137,7 +137,7 @@ CleanCSS constructor accepts a hash as a parameter, i.e.,
 * `advanced` - set to false to disable advanced optimizations - selector & property merging, reduction, etc.
 * `aggressiveMerging` - set to false to disable aggressive merging of properties.
 * `benchmark` - turns on benchmarking mode measuring time spent on cleaning up (run `npm run bench` to see example)
-* `compatibility` - Force compatibility mode to `ie7` or `ie8`. Defaults to not set.
+* `compatibility` - enables compatibility mode, see [below for more examples](#how-to-set-compatibility-mode)
 * `debug` - set to true to get minification statistics under `stats` property (see `test/custom-test.js` for examples)
 * `inliner` - a hash of options for `@import` inliner, see test/protocol-imports-test.js for examples
 * `keepBreaks` - whether to keep line breaks (default is false)
@@ -209,6 +209,33 @@ Clean-css will handle it automatically for you (since version 1.1) in the follow
   2. Use a combination of `relativeTo` and `root` options for absolute rebase (same as 2 in CLI).
   3. `root` takes precendence over `target` as in CLI.
 
+### How to set compatibility mode
+
+Compatibility settings are controlled by `--compatibility` switch (CLI) and `compatibility` option (library mode).
+
+In both modes the following values are allowed:
+
+* `'ie7'` - Internet Explorer 7 compatibility mode
+* `'ie8'` - Internet Explorer 8 compatibility mode
+* `''` or `'*'` (default) - Internet Explorer 9+ compatibility mode
+
+Since clean-css 3 a fine grained control is available over
+[those settings](https://github.com/jakubpawlowicz/clean-css/blob/master/lib/utils/compatibility.js),
+with the following options available:
+
+* `'[+-]colors.opacity'` - - turn on (+) / off (-) `rgba()` / `hsla()` declarations removal
+* `'[+-]properties.iePrefixHack'` - turn on / off IE prefix hack removal
+* `'[+-]properties.ieSuffixHack'` - turn on / off IE suffix hack removal
+* `'[+-]properties.merging'` - turn on / off property merging based on understandability
+* `'[+-]selectors.ie7Hack'` - turn on / off IE7 selector hack removal (`*+html...`)
+* `'[+-]units.rem'` - turn on / off treating `rem` as a proper unit
+
+For example, this declaration `--compatibility 'ie8,+units.rem'` will ensure IE8 compatiblity while enabling `rem` units so the following style `margin:0px 0rem` can be shortened to `margin:0`, while in pure IE8 mode it can't be.
+
+To pass a single off (-) switch in CLI please use the following syntax `--compatibility *,-units.rem`.
+
+In library mode you can also pass `compatiblity` as a hash of options.
+
 ## Acknowledgments (sorted alphabetically)
 
 * Anthony Barre ([@abarre](https://github.com/abarre)) for improvements to
index b8d58c6..b313228 100755 (executable)
@@ -26,7 +26,7 @@ commands
   .option('--skip-advanced', 'Disable advanced optimizations - selector & property merging, reduction, etc.')
   .option('--skip-aggressive-merging', 'Disable properties merging based on their order')
   .option('--rounding-precision [n]', 'Rounds pixel values to `N` decimal places, defaults to 2', parseInt)
-  .option('-c, --compatibility [ie7|ie8]', 'Force compatibility mode')
+  .option('-c, --compatibility [ie7|ie8]', 'Force compatibility mode (see Readme for advanced examples)')
   .option('-t, --timeout [seconds]', 'Per connection timeout when fetching remote @imports (defaults to 5 seconds)')
   .option('-d, --debug', 'Shows debug information (minification time & compression efficiency)');
 
index d8cc23c..0d1dbb3 100644 (file)
@@ -14,6 +14,8 @@ var ExpressionsProcessor = require('./text/expressions-processor');
 var FreeTextProcessor = require('./text/free-text-processor');
 var UrlsProcessor = require('./text/urls-processor');
 
+var Compatibility = require('./utils/compatibility');
+
 var CleanCSS = module.exports = function CleanCSS(options) {
   options = options || {};
 
@@ -21,7 +23,7 @@ var CleanCSS = module.exports = function CleanCSS(options) {
     advanced: options.advanced === undefined ? true : false,
     aggressiveMerging: undefined === options.aggressiveMerging ? true : false,
     benchmark: options.benchmark,
-    compatibility: options.compatibility,
+    compatibility: new Compatibility(options.compatibility).toOptions(),
     debug: options.debug,
     inliner: options.inliner,
     keepBreaks: options.keepBreaks || false,
index 0d321d1..9f4fbc1 100644 (file)
@@ -180,7 +180,7 @@ module.exports = function Optimizer(compatibility, aggressiveMerging, context) {
         property;
       var toOverridePosition = 0;
 
-      if (!compatibility && isIEHack)
+      if (isIEHack && !compatibility.properties.ieSuffixHack)
         continue;
 
       // comment is necessary - we assume that if two properties are one after another
@@ -201,7 +201,7 @@ module.exports = function Optimizer(compatibility, aggressiveMerging, context) {
           if (wasImportant && !isImportant)
             continue tokensLoop;
 
-          if (compatibility && !wasIEHack && isIEHack)
+          if (compatibility.properties.ieSuffixHack && !wasIEHack && isIEHack)
             break;
 
           var _info = processableInfo.processable[_property];
index 3fca0c4..e5b321e 100644 (file)
@@ -90,7 +90,7 @@ module.exports = (function () {
           if (can(matchingComponent.value, token.value)) {
             // The component can override the matching component in the shorthand
 
-            if (compatibility) {
+            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++) {
index 2f37a40..cb3d351 100644 (file)
@@ -2,12 +2,6 @@ var PropertyOptimizer = require('../../properties/optimizer');
 var CleanUp = require('./clean-up');
 var Splitter = require('../../utils/splitter');
 
-var specialSelectors = {
-  '*': /\-(moz|ms|o|webkit)\-/,
-  'ie8': /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not)/,
-  'ie7': /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:focus|:before|:after|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not)/
-};
-
 function AdvancedOptimizer(options, context) {
   this.options = options;
   this.minificationsMade = [];
@@ -15,7 +9,7 @@ function AdvancedOptimizer(options, context) {
 }
 
 AdvancedOptimizer.prototype.isSpecial = function (selector) {
-  return specialSelectors[this.options.compatibility || '*'].test(selector);
+  return this.options.compatibility.selectors.special.test(selector);
 };
 
 AdvancedOptimizer.prototype.removeDuplicates = function (tokens) {
index dac5daf..ee5370a 100644 (file)
@@ -13,7 +13,7 @@ function SimpleOptimizer(options) {
   this.options = options;
 
   var units = ['px', 'em', 'ex', 'cm', 'mm', 'in', 'pt', 'pc', '%'];
-  if (['ie7', 'ie8'].indexOf(options.compatibility) == -1)
+  if (options.compatibility.units.rem)
     units.push('rem');
   options.unitsRegexp = new RegExp('(^|\\s|\\(|,)0(?:' + units.join('|') + ')', 'g');
 
@@ -26,7 +26,7 @@ function SimpleOptimizer(options) {
 }
 
 function removeUnsupported(token, compatibility) {
-  if (compatibility == 'ie7')
+  if (compatibility.selectors.ie7Hack)
     return;
 
   var supported = [];
@@ -160,7 +160,7 @@ function colorMininifier(property, value, compatibility) {
       return colorFunction + '(' + tokens.join(',') + ')';
     });
 
-  if (!compatibility) {
+  if (compatibility.colors.opacity) {
     value = value.replace(/(?:rgba|hsla)\(0,0%?,0%?,0\)/g, function (match) {
       if (new Splitter(',').split(value).pop().indexOf('gradient(') > -1)
         return match;
@@ -182,7 +182,7 @@ function reduce(body, options) {
     var value = token.substring(firstColon + 1);
     var important = false;
 
-    if (!options.compatibility && (property[0] == '_' || property[0] == '*'))
+    if (!options.compatibility.properties.iePrefixHack && (property[0] == '_' || property[0] == '*'))
       continue;
 
     if (value.indexOf('!important') > 0 || value.indexOf('! important') > 0) {
diff --git a/lib/utils/compatibility.js b/lib/utils/compatibility.js
new file mode 100644 (file)
index 0000000..b74a13b
--- /dev/null
@@ -0,0 +1,105 @@
+var util = require('util');
+
+var DEFAULTS = {
+  '*': {
+    colors: {
+      opacity: true // rgba / hsla
+    },
+    properties: {
+      iePrefixHack: false, // underscore / asterisk prefix hacks on IE
+      ieSuffixHack: false, // \9 suffix hacks on IE
+      merging: true // merging properties into one
+    },
+    selectors: {
+      ie7Hack: false, // *+html hack
+      special: /\-(moz|ms|o|webkit)\-/ // special selectors which prevent merging
+    },
+    units: {
+      rem: true
+    }
+  },
+  'ie8': {
+    colors: {
+      opacity: false
+    },
+    properties: {
+      iePrefixHack: true,
+      ieSuffixHack: true,
+      merging: false
+    },
+    selectors: {
+      ie7Hack: false,
+      special: /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not)/
+    },
+    units: {
+      rem: false
+    }
+  },
+  'ie7': {
+    colors: {
+      opacity: false
+    },
+    properties: {
+      iePrefixHack: true,
+      ieSuffixHack: true,
+      merging: false
+    },
+    selectors: {
+      ie7Hack: true,
+      special: /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:focus|:before|:after|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not)/
+    },
+    units: {
+      rem: false
+    }
+  }
+};
+
+function Compatibility(source) {
+  this.source = source || {};
+}
+
+function merge(source, target) {
+  for (var key in source) {
+    var value = source[key];
+
+    if (typeof value === 'object' && !util.isRegExp(value))
+      target[key] = merge(value, target[key] || {});
+    else
+      target[key] = key in target ? target[key] : value;
+  }
+
+  return target;
+}
+
+function calculateSource(source) {
+  if (typeof source == 'object')
+    return source;
+
+  if (!/[,\+\-]/.test(source))
+    return DEFAULTS[source] || DEFAULTS['*'];
+
+  var parts = source.split(',');
+  var template = parts[0] in DEFAULTS ?
+    DEFAULTS[parts.shift()] :
+    DEFAULTS['*'];
+
+  source = {};
+
+  parts.forEach(function (part) {
+    var isAdd = part[0] == '+';
+    var key = part.substring(1).split('.');
+    var group = key[0];
+    var option = key[1];
+
+    source[group] = source[group] || {};
+    source[group][option] = isAdd;
+  });
+
+  return merge(template, source);
+}
+
+Compatibility.prototype.toOptions = function () {
+  return merge(DEFAULTS['*'], calculateSource(this.source));
+};
+
+module.exports = Compatibility;
index f36e874..97acbd3 100644 (file)
@@ -291,6 +291,11 @@ exports.commandsSuite = vows.describe('binary commands').addBatch({
       assert.equal(stdout, readFile('./test/data/unsupported/selectors-ie8.css'));
     }
   }),
+  'custom compatibility': pipedContext('a{_color:red}', '--compatibility "+properties.iePrefixHack"', {
+    'should not transform source': function(error, stdout) {
+      assert.equal(stdout, 'a{_color:red}');
+    }
+  }),
   'rounding precision': {
     defaults: pipedContext('div{width:0.10051px}', '', {
       'should keep 2 decimal places': function(error, stdout) {
index 5ec0ddd..1f5bf50 100644 (file)
@@ -1,9 +1,12 @@
 var vows = require('vows');
 var assert = require('assert');
 var SelectorsOptimizer = require('../../lib/selectors/optimizer');
+var Compatibility = require('../../lib/utils/compatibility');
 
 function optimizerContext(group, specs, options) {
   var context = {};
+  options = options || {};
+  options.compatibility = new Compatibility(options.compatibility).toOptions();
 
   function optimized(target) {
     return function (source) {
index 0ab07a9..c7364f7 100644 (file)
@@ -3,10 +3,12 @@ var assert = require('assert');
 
 var Tokenizer = require('../../../lib/selectors/tokenizer');
 var SimpleOptimizer = require('../../../lib/selectors/optimizers/simple');
+var Compatibility = require('../../../lib/utils/compatibility');
 
 function selectorContext(group, specs, options) {
   var context = {};
   options = options || {};
+  options.compatibility = new Compatibility(options.compatibility).toOptions();
 
   function optimized(selectors) {
     return function (source) {
@@ -30,6 +32,7 @@ function selectorContext(group, specs, options) {
 function propertyContext(group, specs, options) {
   var context = {};
   options = options || {};
+  options.compatibility = new Compatibility(options.compatibility).toOptions();
 
   function optimized(selectors) {
     return function (source) {
diff --git a/test/utils/compatibility-test.js b/test/utils/compatibility-test.js
new file mode 100644 (file)
index 0000000..8206ad6
--- /dev/null
@@ -0,0 +1,108 @@
+var vows = require('vows');
+var assert = require('assert');
+var Compatibility = require('../../lib/utils/compatibility');
+
+vows.describe(Compatibility)
+  .addBatch({
+    'as an empty hash': {
+      topic: new Compatibility({}).toOptions(),
+      'gets default options': function(options) {
+        assert.isFalse(options.properties.iePrefixHack);
+        assert.isFalse(options.properties.ieSuffixHack);
+        assert.isFalse(options.selectors.ie7Hack);
+        assert.isTrue(options.properties.merging);
+        assert.isTrue(options.units.rem);
+        assert.isTrue(options.colors.opacity);
+        assert.deepEqual(options.selectors.special, /\-(moz|ms|o|webkit)\-/);
+      }
+    },
+    'not given': {
+      topic: new Compatibility().toOptions(),
+      'gets default options': function(options) {
+        assert.deepEqual(options, new Compatibility({}).toOptions());
+      }
+    },
+    'as a populated hash': {
+      topic: new Compatibility({ units: { rem: false }, properties: { prefix: true } }).toOptions(),
+      'gets merged options': function(options) {
+        assert.isFalse(options.properties.iePrefixHack);
+        assert.isFalse(options.properties.ieSuffixHack);
+        assert.isFalse(options.selectors.ie7Hack);
+        assert.isTrue(options.properties.merging);
+        assert.isFalse(options.units.rem);
+        assert.isTrue(options.colors.opacity);
+        assert.deepEqual(options.selectors.special, /\-(moz|ms|o|webkit)\-/);
+      }
+    }
+  })
+  .addBatch({
+    'as an ie8 template': {
+      topic: new Compatibility('ie8').toOptions(),
+      'gets template options': function(options) {
+        assert.isTrue(options.properties.iePrefixHack);
+        assert.isTrue(options.properties.ieSuffixHack);
+        assert.isFalse(options.selectors.ie7Hack);
+        assert.isFalse(options.properties.merging);
+        assert.isFalse(options.units.rem);
+        assert.isFalse(options.colors.opacity);
+        assert.deepEqual(options.selectors.special, /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not)/);
+      }
+    },
+    'as an ie7 template': {
+      topic: new Compatibility('ie7').toOptions(),
+      'gets template options': function(options) {
+        assert.isTrue(options.properties.iePrefixHack);
+        assert.isTrue(options.properties.ieSuffixHack);
+        assert.isTrue(options.selectors.ie7Hack);
+        assert.isFalse(options.properties.merging);
+        assert.isFalse(options.units.rem);
+        assert.isFalse(options.colors.opacity);
+        assert.deepEqual(options.selectors.special, /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:focus|:before|:after|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not)/);
+      }
+    },
+    'as an unknown template': {
+      topic: new Compatibility('').toOptions(),
+      'gets default options': function(options) {
+        assert.deepEqual(options, new Compatibility({}).toOptions());
+      }
+    }
+  })
+  .addBatch({
+    'as a complex string value with group': {
+      topic: new Compatibility('ie8,-properties.iePrefixHack,+colors.opacity').toOptions(),
+      'gets calculated options': function(options) {
+        assert.isFalse(options.properties.iePrefixHack);
+        assert.isTrue(options.properties.ieSuffixHack);
+        assert.isFalse(options.selectors.ie7Hack);
+        assert.isFalse(options.properties.merging);
+        assert.isFalse(options.units.rem);
+        assert.isTrue(options.colors.opacity);
+        assert.deepEqual(options.selectors.special, /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not)/);
+      }
+    },
+    'as a single string value without group': {
+      topic: new Compatibility('+properties.iePrefixHack').toOptions(),
+      'gets calculated options': function(options) {
+        assert.isTrue(options.properties.iePrefixHack);
+        assert.isFalse(options.properties.ieSuffixHack);
+        assert.isFalse(options.selectors.ie7Hack);
+        assert.isTrue(options.properties.merging);
+        assert.isTrue(options.units.rem);
+        assert.isTrue(options.colors.opacity);
+        assert.deepEqual(options.selectors.special, /\-(moz|ms|o|webkit)\-/);
+      }
+    },
+    'as a complex string value without group': {
+      topic: new Compatibility('+properties.iePrefixHack,-units.rem').toOptions(),
+      'gets calculated options': function(options) {
+        assert.isTrue(options.properties.iePrefixHack);
+        assert.isFalse(options.properties.ieSuffixHack);
+        assert.isFalse(options.selectors.ie7Hack);
+        assert.isTrue(options.properties.merging);
+        assert.isFalse(options.units.rem);
+        assert.isTrue(options.colors.opacity);
+        assert.deepEqual(options.selectors.special, /\-(moz|ms|o|webkit)\-/);
+      }
+    }
+  })
+  .export(module);