From: Jakub Pawlowicz Date: Thu, 19 Jan 2017 19:50:59 +0000 (+0100) Subject: Fixes #863 - adds `transform` callback for custom optimizations. X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=51846893a2fca57015b682b7f8a49953168a14aa;p=clean-css.git Fixes #863 - adds `transform` callback for custom optimizations. Why: * So users can apply custom optimizations without forking clean-css and learning how to plug such optimization in; * may also aid in rewriting URLs after introduction of `rebaseTo`. --- diff --git a/History.md b/History.md index 8baf029b..7ae329ea 100644 --- a/History.md +++ b/History.md @@ -45,6 +45,7 @@ * 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) ================== diff --git a/README.md b/README.md index 24d84e77..c8e95f5d 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,8 @@ new CleanCSS({ 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 } } }); @@ -359,6 +360,30 @@ new CleanCSS({ returnPromise: true }) .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) diff --git a/lib/optimizer/level-1/optimize.js b/lib/optimizer/level-1/optimize.js index 808e94a6..2970a8a6 100644 --- a/lib/optimizer/level-1/optimize.js +++ b/lib/optimizer/level-1/optimize.js @@ -19,6 +19,8 @@ var Marker = require('../../tokenizer/marker'); 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'); @@ -403,6 +405,18 @@ function removeUrlQuotes(value) { 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) { @@ -413,6 +427,7 @@ 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; @@ -504,6 +519,13 @@ function optimizeBody(properties, context) { } } + value = transformValue(name, value, levelOptions.transform); + + if (value === IgnoreProperty) { + property.unused = true; + continue propertyLoop; + } + property.value[j][1] = value; } diff --git a/lib/options/optimization-level.js b/lib/options/optimization-level.js index 33948bb1..70d45c8a 100644 --- a/lib/options/optimization-level.js +++ b/lib/options/optimization-level.js @@ -30,7 +30,8 @@ DEFAULTS[OptimizationLevel.One] = { specialComments: 'all', tidyAtRules: true, tidyBlockScopes: true, - tidySelectors: true + tidySelectors: true, + transform: noop }; DEFAULTS[OptimizationLevel.Two] = { mergeAdjacentRules: true, @@ -56,6 +57,8 @@ var TRUE_KEYWORD_2 = 'on'; var OPTION_SEPARATOR = ';'; var OPTION_VALUE_SEPARATOR = ':'; +function noop() {} + function optimizationLevelFrom(source) { var level = override(DEFAULTS, {}); var Zero = OptimizationLevel.Zero; diff --git a/lib/utils/override.js b/lib/utils/override.js index 86bf94c3..e7f84948 100644 --- a/lib/utils/override.js +++ b/lib/utils/override.js @@ -1,17 +1,30 @@ 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; } } diff --git a/test/module-test.js b/test/module-test.js index f5aee065..357a1211 100644 --- a/test/module-test.js +++ b/test/module-test.js @@ -410,6 +410,63 @@ vows.describe('module tests').addBatch({ 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': { diff --git a/test/options/optimization-level-test.js b/test/options/optimization-level-test.js index 09072566..ae3805b3 100644 --- a/test/options/optimization-level-test.js +++ b/test/options/optimization-level-test.js @@ -18,6 +18,9 @@ vows.describe(optimizationLevelFrom) 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, @@ -64,6 +67,9 @@ vows.describe(optimizationLevelFrom) 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, @@ -99,6 +105,9 @@ vows.describe(optimizationLevelFrom) 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, @@ -160,6 +169,9 @@ vows.describe(optimizationLevelFrom) 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, @@ -210,6 +222,9 @@ vows.describe(optimizationLevelFrom) 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, @@ -245,6 +260,9 @@ vows.describe(optimizationLevelFrom) 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, @@ -280,6 +298,9 @@ vows.describe(optimizationLevelFrom) 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, @@ -330,6 +351,9 @@ vows.describe(optimizationLevelFrom) 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, @@ -380,6 +404,9 @@ vows.describe(optimizationLevelFrom) 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, @@ -415,6 +442,9 @@ vows.describe(optimizationLevelFrom) 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, @@ -465,6 +495,9 @@ vows.describe(optimizationLevelFrom) 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, @@ -515,6 +548,9 @@ vows.describe(optimizationLevelFrom) 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, @@ -565,6 +601,9 @@ vows.describe(optimizationLevelFrom) 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, @@ -615,6 +654,9 @@ vows.describe(optimizationLevelFrom) 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, @@ -650,6 +692,9 @@ vows.describe(optimizationLevelFrom) 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, @@ -702,6 +747,9 @@ vows.describe(optimizationLevelFrom) 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,