* Manages compatibility options as a hash of options.
* Handles fallback to previous compatibility options.
[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.
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)
```
* `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)
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
.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)');
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 || {};
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,
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
if (wasImportant && !isImportant)
continue tokensLoop;
- if (compatibility && !wasIEHack && isIEHack)
+ if (compatibility.properties.ieSuffixHack && !wasIEHack && isIEHack)
break;
var _info = processableInfo.processable[_property];
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++) {
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 = [];
}
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) {
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');
}
function removeUnsupported(token, compatibility) {
- if (compatibility == 'ie7')
+ if (compatibility.selectors.ie7Hack)
return;
var supported = [];
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;
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) {
--- /dev/null
+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;
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) {
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) {
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) {
function propertyContext(group, specs, options) {
var context = {};
options = options || {};
+ options.compatibility = new Compatibility(options.compatibility).toOptions();
function optimized(selectors) {
return function (source) {
--- /dev/null
+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);