* Fixed issue [#847](https://github.com/jakubpawlowicz/clean-css/issues/847) - regression in handling invalid selectors.
* Fixed issue [#849](https://github.com/jakubpawlowicz/clean-css/issues/849) - disables inlining protocol-less resources.
* Fixed issue [#857](https://github.com/jakubpawlowicz/clean-css/issues/857) - normalizes CleanCSS API interface.
+* Fixed issue [#863](https://github.com/jakubpawlowicz/clean-css/issues/863) - adds `transform` callback for custom optimizations.
[3.4.23 / 2016-12-17](https://github.com/jakubpawlowicz/clean-css/compare/v3.4.22...v3.4.23)
==================
specialComments: 'all', // denotes a number of /*! ... */ comments preserved; defaults to `all`
tidyAtRules: true, // controls at-rules (e.g. `@charset`, `@import`) optimizing; defaults to `true`
tidyBlockScopes: true, // controls block scopes (e.g. `@media`) optimizing; defaults to `true`
- tidySelectors: true // controls selectors optimizing; defaults to `true`
+ tidySelectors: true, // controls selectors optimizing; defaults to `true`,
+ transform: function () {} // defines a callback for fine-grained property optimization; defaults to no-op
}
}
});
.catch(function (error) { // deal with errors });
```
+#### How to apply arbitrary transformations to CSS properties
+
+If clean-css doesn't perform a specific property optimization, you can use `transform` callback to apply it:
+
+```js
+var CleanCSS = require('clean-css');
+var source = '.block{background-image:url(/path/to/image.png)}';
+var output = new CleanCSS({
+ level: {
+ 1: {
+ transform: function (propertyName, propertyValue) {
+ if (propertyName == 'background-image' && propertyValue.indexOf('/path/to') > -1) {
+ return propertyValue.replace('/path/to', '../valid/path/to');
+ }
+ }
+ }
+ }
+}).minify(source);
+
+console.log(output.styles); # => .block{background-image:url(../valid/path/to/image.png)}
+```
+
+Note: returning `false` from `transform` callback will drop a property.
+
### How to use clean-css with build tools?
* [Broccoli](https://github.com/broccolijs/broccoli#broccoli): [broccoli-clean-css](https://github.com/shinnn/broccoli-clean-css)
var formatPosition = require('../../utils/format-position');
var split = require('../../utils/split');
+var IgnoreProperty = 'ignore-property';
+
var CHARSET_TOKEN = '@charset';
var CHARSET_REGEXP = new RegExp('^' + CHARSET_TOKEN, 'i');
value;
}
+function transformValue(propertyName, propertyValue, transformCallback) {
+ var transformedValue = transformCallback(propertyName, propertyValue);
+
+ if (transformedValue === undefined) {
+ return propertyValue;
+ } else if (transformedValue === false) {
+ return IgnoreProperty;
+ } else {
+ return transformedValue;
+ }
+}
+
//
function optimizeBody(properties, context) {
var propertyToken;
var _properties = wrapForOptimizing(properties, true);
+ propertyLoop:
for (var i = 0, l = _properties.length; i < l; i++) {
property = _properties[i];
name = property.name;
}
}
+ value = transformValue(name, value, levelOptions.transform);
+
+ if (value === IgnoreProperty) {
+ property.unused = true;
+ continue propertyLoop;
+ }
+
property.value[j][1] = value;
}
specialComments: 'all',
tidyAtRules: true,
tidyBlockScopes: true,
- tidySelectors: true
+ tidySelectors: true,
+ transform: noop
};
DEFAULTS[OptimizationLevel.Two] = {
mergeAdjacentRules: true,
var OPTION_SEPARATOR = ';';
var OPTION_VALUE_SEPARATOR = ':';
+function noop() {}
+
function optimizationLevelFrom(source) {
var level = override(DEFAULTS, {});
var Zero = OptimizationLevel.Zero;
function override(source1, source2) {
var target = {};
+ var key1;
+ var key2;
+ var item;
- for (var key1 in source1) {
- target[key1] = source1[key1];
+ for (key1 in source1) {
+ item = source1[key1];
+
+ if (Array.isArray(item)) {
+ target[key1] = item.slice(0);
+ } else if (typeof item == 'object' && item !== null) {
+ target[key1] = override(item, {});
+ } else {
+ target[key1] = item;
+ }
}
- for (var key2 in source2) {
- if (key2 in target && typeof source2[key2] == 'object') {
- target[key2] = override(target[key2], source2[key2]);
- } else if (Array.isArray(source2[key2])) {
- target[key2] = source2[key2].slice(0);
+ for (key2 in source2) {
+ item = source2[key2];
+
+ if (key2 in target && Array.isArray(item)) {
+ target[key2] = item.slice(0);
+ } else if (key2 in target && typeof item == 'object' && item !== null) {
+ target[key2] = override(target[key2], item);
} else {
- target[key2] = source2[key2];
+ target[key2] = item;
}
}
assert.instanceOf(minified.sourceMap, SourceMapGenerator);
}
},
+ 'arbitrary property transformations': {
+ 'allows changing property value': {
+ 'topic': function () {
+ return new CleanCSS({
+ level: {
+ 1: {
+ transform: function (propertyName, propertyValue) {
+ if (propertyName == 'background-image' && propertyValue.indexOf('/path/to') > -1) {
+ return propertyValue.replace('/path/to', '../valid/path/to');
+ }
+ }
+ }
+ }
+ }).minify('.block{background-image:url(/path/to/image.png);border-image:url(image.png)}');
+ },
+ 'gives right output': function (error, output) {
+ assert.equal(output.styles, '.block{background-image:url(../valid/path/to/image.png);border-image:url(image.png)}');
+ }
+ },
+ 'allows dropping properties': {
+ 'topic': function () {
+ return new CleanCSS({
+ level: {
+ 1: {
+ transform: function (propertyName) {
+ if (propertyName.indexOf('-o-') === 0) {
+ return false;
+ }
+ }
+ }
+ }
+ }).minify('.block{-o-border-radius:2px;border-image:url(image.png)}');
+ },
+ 'gives right output': function (error, output) {
+ assert.equal(output.styles, '.block{border-image:url(image.png)}');
+ }
+ },
+ 'combined with level 2 optimization': {
+ 'topic': function () {
+ return new CleanCSS({
+ level: {
+ 1: {
+ transform: function (propertyName) {
+ if (propertyName == 'margin-bottom') {
+ return false;
+ }
+ }
+ },
+ 2: true
+ }
+ }).minify('.block{-o-border-radius:2px;margin:0 12px;margin-bottom:5px}');
+ },
+ 'drops property before level 2 optimizations': function (error, output) {
+ assert.equal(output.styles, '.block{-o-border-radius:2px;margin:0 12px}');
+ }
+ }
+ },
'accepts a list of source files as array': {
'relative': {
'with rebase to the current directory': {
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: true,
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: true,
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: true,
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: true,
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: false,
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: false,
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: true,
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: true,
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: true,
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: true,
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: true,
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: true,
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: true,
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: true,
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: true,
assert.deepEqual(levelOptions['0'], {});
},
'has level 1 options': function (levelOptions) {
+ assert.isTrue(typeof levelOptions['1'].transform == 'function');
+ delete levelOptions['1'].transform;
+
assert.deepEqual(levelOptions['1'], {
cleanupCharsets: true,
normalizeUrls: true,