From cb620b3304059df828344807e9a8628113229102 Mon Sep 17 00:00:00 2001 From: Jakub Pawlowicz Date: Tue, 14 Oct 2014 08:09:07 +0100 Subject: [PATCH] Adds more granular control over compatibility settings. * Manages compatibility options as a hash of options. * Handles fallback to previous compatibility options. --- History.md | 1 + README.md | 31 ++++++- bin/cleancss | 2 +- lib/clean.js | 4 +- lib/properties/optimizer.js | 4 +- lib/properties/override-compactor.js | 2 +- lib/selectors/optimizers/advanced.js | 8 +- lib/selectors/optimizers/simple.js | 8 +- lib/utils/compatibility.js | 105 ++++++++++++++++++++++ test/binary-test.js | 5 ++ test/selectors/optimizer-test.js | 3 + test/selectors/optimizers/simple-test.js | 3 + test/utils/compatibility-test.js | 108 +++++++++++++++++++++++ 13 files changed, 266 insertions(+), 18 deletions(-) create mode 100644 lib/utils/compatibility.js create mode 100644 test/utils/compatibility-test.js diff --git a/History.md b/History.md index 8498c609..69c386bf 100644 --- a/History.md +++ b/History.md @@ -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. diff --git a/README.md b/README.md index 3b7890af..875bebb5 100644 --- 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 diff --git a/bin/cleancss b/bin/cleancss index b8d58c63..b3132284 100755 --- a/bin/cleancss +++ b/bin/cleancss @@ -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)'); diff --git a/lib/clean.js b/lib/clean.js index d8cc23c2..0d1dbb3d 100644 --- a/lib/clean.js +++ b/lib/clean.js @@ -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, diff --git a/lib/properties/optimizer.js b/lib/properties/optimizer.js index 0d321d1a..9f4fbc16 100644 --- a/lib/properties/optimizer.js +++ b/lib/properties/optimizer.js @@ -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]; diff --git a/lib/properties/override-compactor.js b/lib/properties/override-compactor.js index 3fca0c40..e5b321e9 100644 --- a/lib/properties/override-compactor.js +++ b/lib/properties/override-compactor.js @@ -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++) { diff --git a/lib/selectors/optimizers/advanced.js b/lib/selectors/optimizers/advanced.js index 2f37a40c..cb3d3516 100644 --- a/lib/selectors/optimizers/advanced.js +++ b/lib/selectors/optimizers/advanced.js @@ -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) { diff --git a/lib/selectors/optimizers/simple.js b/lib/selectors/optimizers/simple.js index dac5daf8..ee5370a6 100644 --- a/lib/selectors/optimizers/simple.js +++ b/lib/selectors/optimizers/simple.js @@ -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 index 00000000..b74a13b3 --- /dev/null +++ b/lib/utils/compatibility.js @@ -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; diff --git a/test/binary-test.js b/test/binary-test.js index f36e8742..97acbd3e 100644 --- a/test/binary-test.js +++ b/test/binary-test.js @@ -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) { diff --git a/test/selectors/optimizer-test.js b/test/selectors/optimizer-test.js index 5ec0ddd0..1f5bf50e 100644 --- a/test/selectors/optimizer-test.js +++ b/test/selectors/optimizer-test.js @@ -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) { diff --git a/test/selectors/optimizers/simple-test.js b/test/selectors/optimizers/simple-test.js index 0ab07a94..c7364f76 100644 --- a/test/selectors/optimizers/simple-test.js +++ b/test/selectors/optimizers/simple-test.js @@ -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 index 00000000..8206ad62 --- /dev/null +++ b/test/utils/compatibility-test.js @@ -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); -- 2.34.1