From 0b81de23105b52b3235635052b0102cfbe6cfc91 Mon Sep 17 00:00:00 2001 From: Jakub Pawlowicz Date: Mon, 9 Jan 2017 13:52:43 +0100 Subject: [PATCH] See #731 - adds granular control over level 2 optimizations. Why: * So users can selectively disable certain optimizations if they want to. --- History.md | 1 + README.md | 28 +++++++---- bin/cleancss | 14 ++++-- lib/optimizer/level-2/optimize.js | 46 ++++++++++++++---- lib/options/optimization-level.js | 6 +++ test/optimizer/level-2/merge-adjacent-test.js | 8 ++++ .../merge-non-adjacent-by-body-test.js | 16 +++++++ .../merge-non-adjacent-by-selector-test.js | 16 +++++++ .../level-2/reduce-non-adjacent-test.js | 8 ++++ .../remove-duplicate-font-at-rules-test.js | 8 ++++ .../remove-duplicate-media-queries-test.js | 12 ++++- .../level-2/remove-duplicates-test.js | 8 ++++ test/options/optimization-level-test.js | 48 +++++++++++++++++++ 13 files changed, 195 insertions(+), 24 deletions(-) diff --git a/History.md b/History.md index c323c237..041dcf99 100644 --- a/History.md +++ b/History.md @@ -19,6 +19,7 @@ * Fixed issue [#685](https://github.com/jakubpawlowicz/clean-css/issues/685) - adds lowercasing hex colors optimization. * Fixed issue [#686](https://github.com/jakubpawlowicz/clean-css/issues/686) - adds rounding precision for all units. * Fixed issue [#703](https://github.com/jakubpawlowicz/clean-css/issues/703) - changes default IE compatibility to 10+. +* Fixed issue [#731](https://github.com/jakubpawlowicz/clean-css/issues/731) - adds granular control over level 2 optimizations. * Fixed issue [#739](https://github.com/jakubpawlowicz/clean-css/issues/739) - error when a closing brace is missing. * Fixed issue [#750](https://github.com/jakubpawlowicz/clean-css/issues/750) - allows `width` overriding. * Fixed issue [#756](https://github.com/jakubpawlowicz/clean-css/issues/756) - adds disabling font-weight optimizations. diff --git a/README.md b/README.md index 70b898d2..5bc4cfa6 100644 --- a/README.md +++ b/README.md @@ -129,10 +129,16 @@ Level 2 optimizations: ```bash cleancss -O2 one.css cleancss -O2 mediaMerging:off;restructuring:off;semanticMerging:on;shorthandCompacting:off one.css -# `mediaMerging` controls `@media` merging behavior; defaults to `on` (alias to `true`) -# `restructuring` controls content restructuring behavior; defaults `off` (alias to `false`) -# `semanticMerging` controls semantic merging behavior; defaults to `off` (alias to `false`) -# `shorthandCompacting` controls shorthand compacting behavior; defaults to `on` (alias to `true`) +# `adjacentRulesMerging` controls adjacent rules merging; defaults to `on` +# `duplicateFontRulesRemoving` controls duplicate `@font-face` removing; defaults to `on` +# `duplicateMediaRemoving` controls duplicate `@media` removing; defaults to `on` +# `duplicateRulesRemoving` controls duplicate rules removing; defaults to `on` +# `mediaMerging` controls `@media` merging; defaults to `on` +# `nonAdjacentRulesMerging` controls non-adjacent rule merging; defaults to `on` +# `nonAdjacentRulesReducing` controls non-adjacent rule reducing; defaults to `on` +# `restructuring` controls content restructuring; defaults to `off` +# `semanticMerging` controls semantic merging; defaults to `off` +# `shorthandCompacting` controls shorthand compacting; defaults to `on` ``` ### How to use clean-css API? @@ -194,10 +200,16 @@ new CleanCSS({ new CleanCSS({ level: { 2: { - mediaMerging: true, // controls `@media` merging behavior; defaults to true - restructuring: false, // controls content restructuring behavior; defaults to false - semanticMerging: false, // controls semantic merging behavior; defaults to false - shorthandCompacting: true // controls shorthand compacting behavior; defaults to true + adjacentRulesMerging: true, // controls adjacent rules merging; defaults to true + duplicateFontRulesRemoving: true, // controls duplicate `@font-face` removing; defaults to true + duplicateMediaRemoving: true, // controls duplicate `@media` removing; defaults to true + duplicateRulesRemoving: true, // controls duplicate rules removing; defaults to true + mediaMerging: true, // controls `@media` merging; defaults to true + nonAdjacentRulesMerging: true, // controls non-adjacent rule merging; defaults to true + nonAdjacentRulesReducing: true, // controls non-adjacent rule reducing; defaults to true + restructuring: false, // controls content restructuring; defaults to false + semanticMerging: false, // controls semantic merging; defaults to false + shorthandCompacting: true, // controls shorthand compacting; defaults to true } } }); diff --git a/bin/cleancss b/bin/cleancss index fd3c7ac4..5a5b4dc9 100755 --- a/bin/cleancss +++ b/bin/cleancss @@ -45,10 +45,16 @@ commands.on('--help', function () { console.log(' Level 2 optimizations:'); console.log(' %> cleancss -O2 one.css'); console.log(' %> cleancss -O2 mediaMerging:off;restructuring:off;semanticMerging:on;shorthandCompacting:off one.css'); - console.log(' %> # `mediaMerging` controls `@media` merging behavior; defaults to `on` (alias to `true`)'); - console.log(' %> # `restructuring` controls content restructuring behavior; defaults to `off` (alias to `false`)'); - console.log(' %> # `semanticMerging` controls semantic merging behavior; defaults to `off` (alias to `false`)'); - console.log(' %> # `shorthandCompacting` controls shorthand compacting behavior; defaults to `on` (alias to `true`)'); + console.log(' %> # `adjacentRulesMerging` controls adjacent rules merging; defaults to `on`'); + console.log(' %> # `duplicateFontRulesRemoving` controls duplicate `@font-face` removing; defaults to `on`'); + console.log(' %> # `duplicateMediaRemoving` controls duplicate `@media` removing; defaults to `on`'); + console.log(' %> # `duplicateRulesRemoving` controls duplicate rules removing; defaults to `on`'); + console.log(' %> # `mediaMerging` controls `@media` merging; defaults to `on`'); + console.log(' %> # `nonAdjacentRulesMerging` controls non-adjacent rule merging; defaults to `on`'); + console.log(' %> # `nonAdjacentRulesReducing` controls non-adjacent rule reducing; defaults to `on`'); + console.log(' %> # `restructuring` controls content restructuring; defaults to `off`'); + console.log(' %> # `semanticMerging` controls semantic merging; defaults to `off`'); + console.log(' %> # `shorthandCompacting` controls shorthand compacting; defaults to `on`'); process.exit(); }); diff --git a/lib/optimizer/level-2/optimize.js b/lib/optimizer/level-2/optimize.js index 77ce1c91..546a759a 100644 --- a/lib/optimizer/level-2/optimize.js +++ b/lib/optimizer/level-2/optimize.js @@ -65,27 +65,53 @@ function recursivelyOptimizeProperties(tokens, context) { } function level2Optimize(tokens, context, withRestructuring) { + var levelOptions = context.options.level[OptimizationLevel.Two]; + var reduced; + var i; + recursivelyOptimizeBlocks(tokens, context); recursivelyOptimizeProperties(tokens, context); - removeDuplicates(tokens, context); - mergeAdjacent(tokens, context); - reduceNonAdjacent(tokens, context); + if (levelOptions.duplicateRulesRemoving) { + removeDuplicates(tokens, context); + } + + if (levelOptions.adjacentRulesMerging) { + mergeAdjacent(tokens, context); + } + + if (levelOptions.nonAdjacentRulesReducing) { + reduceNonAdjacent(tokens, context); + } + + if (levelOptions.nonAdjacentRulesMerging && levelOptions.nonAdjacentRulesMerging != 'body') { + mergeNonAdjacentBySelector(tokens, context); + } - mergeNonAdjacentBySelector(tokens, context); - mergeNonAdjacentByBody(tokens, context); + if (levelOptions.nonAdjacentRulesMerging && levelOptions.nonAdjacentRulesMerging != 'selector') { + mergeNonAdjacentByBody(tokens, context); + } - if (context.options.level[OptimizationLevel.Two].restructuring && withRestructuring) { + if (levelOptions.restructuring && levelOptions.adjacentRulesMerging && withRestructuring) { restructure(tokens, context); mergeAdjacent(tokens, context); } - removeDuplicateFontAtRules(tokens, context); + if (levelOptions.restructuring && !levelOptions.adjacentRulesMerging && withRestructuring) { + restructure(tokens, context); + } + + if (levelOptions.duplicateFontRulesRemoving) { + removeDuplicateFontAtRules(tokens, context); + } - if (context.options.level[OptimizationLevel.Two].mediaMerging) { + if (levelOptions.duplicateMediaRemoving) { removeDuplicateMediaQueries(tokens, context); - var reduced = mergeMediaQueries(tokens, context); - for (var i = reduced.length - 1; i >= 0; i--) { + } + + if (levelOptions.mediaMerging) { + reduced = mergeMediaQueries(tokens, context); + for (i = reduced.length - 1; i >= 0; i--) { level2Optimize(reduced[i][2], context, false); } } diff --git a/lib/options/optimization-level.js b/lib/options/optimization-level.js index c708af9d..74e24f78 100644 --- a/lib/options/optimization-level.js +++ b/lib/options/optimization-level.js @@ -15,7 +15,13 @@ DEFAULTS[OptimizationLevel.One] = { specialComments: 'all' }; DEFAULTS[OptimizationLevel.Two] = { + adjacentRulesMerging: true, + duplicateFontRulesRemoving: true, + duplicateMediaRemoving: true, + duplicateRulesRemoving: true, mediaMerging: true, + nonAdjacentRulesMerging: true, + nonAdjacentRulesReducing: true, restructuring: false, semanticMerging: false, shorthandCompacting: true diff --git a/test/optimizer/level-2/merge-adjacent-test.js b/test/optimizer/level-2/merge-adjacent-test.js index 87c1ee13..62dca5e0 100644 --- a/test/optimizer/level-2/merge-adjacent-test.js +++ b/test/optimizer/level-2/merge-adjacent-test.js @@ -98,6 +98,14 @@ vows.describe('remove duplicates') ], }, { level: { 2: { restructuring: true } } }) ) + .addBatch( + optimizerContext('with level 2 off but only adjacentRuleMerging on', { + 'same context': [ + 'a{background:url(image.png)}a{display:block;width:75px;background-repeat:no-repeat}', + 'a{background:url(image.png);display:block;width:75px;background-repeat:no-repeat}', + ], + }, { level: { 2: { all: false, adjacentRulesMerging: true } } }) + ) .addBatch( optimizerContext('with level 2 off', { 'same context': [ diff --git a/test/optimizer/level-2/merge-non-adjacent-by-body-test.js b/test/optimizer/level-2/merge-non-adjacent-by-body-test.js index 16eaedd0..e995977f 100644 --- a/test/optimizer/level-2/merge-non-adjacent-by-body-test.js +++ b/test/optimizer/level-2/merge-non-adjacent-by-body-test.js @@ -34,6 +34,22 @@ vows.describe('merge non djacent by body') ] }, { level: 2 }) ) + .addBatch( + optimizerContext('with level 2 off but nonAdjacentRulesMerging on', { + 'of element selectors': [ + 'p{color:red}div{display:block}span{color:red}', + 'p,span{color:red}div{display:block}' + ] + }, { level: { 2: { all: false, nonAdjacentRulesMerging: true } } }) + ) + .addBatch( + optimizerContext('with level 2 off but nonAdjacentRulesMerging set to selector', { + 'of element selectors': [ + 'p{color:red}div{display:block}span{color:red}', + 'p{color:red}div{display:block}span{color:red}' + ] + }, { level: { 2: { all: false, nonAdjacentRulesMerging: 'selector' } } }) + ) .addBatch( optimizerContext('with level 2 off', { 'with repeated selectors': [ diff --git a/test/optimizer/level-2/merge-non-adjacent-by-selector-test.js b/test/optimizer/level-2/merge-non-adjacent-by-selector-test.js index 55ec41d7..6f1414f0 100644 --- a/test/optimizer/level-2/merge-non-adjacent-by-selector-test.js +++ b/test/optimizer/level-2/merge-non-adjacent-by-selector-test.js @@ -18,6 +18,22 @@ vows.describe('merge non djacent by selector') // ] }, { level: 2 }) ) + .addBatch( + optimizerContext('with level 2 off but nonAdjacentRulesMerging on', { + 'of element selectors': [ + '.one{color:red}.two{color:#fff}.one{font-weight:400}', + '.one{color:red;font-weight:400}.two{color:#fff}' + ] + }, { level: { 2: { all: false, nonAdjacentRulesMerging: true } } }) + ) + .addBatch( + optimizerContext('with level 2 off but nonAdjacentRulesMerging set to body', { + 'of element selectors': [ + '.one{color:red}.two{color:#fff}.one{font-weight:400}', + '.one{color:red}.two{color:#fff}.one{font-weight:400}' + ] + }, { level: { 2: { all: false, nonAdjacentRulesMerging: 'body' } } }) + ) .addBatch( optimizerContext('level 2 off', { 'up': [ diff --git a/test/optimizer/level-2/reduce-non-adjacent-test.js b/test/optimizer/level-2/reduce-non-adjacent-test.js index df8d945e..97097352 100644 --- a/test/optimizer/level-2/reduce-non-adjacent-test.js +++ b/test/optimizer/level-2/reduce-non-adjacent-test.js @@ -126,6 +126,14 @@ vows.describe('remove duplicates') ] }, { aggressiveMerging: false, level: { 2: { restructuring: true } } }) ) + .addBatch( + optimizerContext('level 2 off but nonAdjacentRulesReducing on', { + 'non-adjacent with multi selectors': [ + 'a{padding:10px;margin:0;color:red}.one{color:red}a,p{color:red;padding:0}', + 'a{margin:0;color:red}.one{color:red}a,p{color:red;padding:0}' + ] + }, { level: { 2: { all: false, nonAdjacentRulesReducing: true } } }) + ) .addBatch( optimizerContext('level 2 off', { 'non-adjacent': [ diff --git a/test/optimizer/level-2/remove-duplicate-font-at-rules-test.js b/test/optimizer/level-2/remove-duplicate-font-at-rules-test.js index 20e9a622..7ed12ebc 100644 --- a/test/optimizer/level-2/remove-duplicate-font-at-rules-test.js +++ b/test/optimizer/level-2/remove-duplicate-font-at-rules-test.js @@ -18,6 +18,14 @@ vows.describe('remove duplicate @font-face at-rules') ] }, { level: 2 }) ) + .addBatch( + optimizerContext('level 2 off but duplicateFontRulesRemoving on', { + 'non-adjacent': [ + '@font-face{font-family:test;src:url(fonts/test.woff2)}.one{color:red}@font-face{font-family:test;src:url(fonts/test.woff2)}', + '@font-face{font-family:test;src:url(fonts/test.woff2)}.one{color:red}' + ] + }, { level: { 2: { all: false, duplicateFontRulesRemoving: true } } }) + ) .addBatch( optimizerContext('level 2 off', { 'keeps content same': [ diff --git a/test/optimizer/level-2/remove-duplicate-media-queries-test.js b/test/optimizer/level-2/remove-duplicate-media-queries-test.js index 20b6df69..cfa9e510 100644 --- a/test/optimizer/level-2/remove-duplicate-media-queries-test.js +++ b/test/optimizer/level-2/remove-duplicate-media-queries-test.js @@ -22,6 +22,14 @@ vows.describe('remove duplicate media queries') ] }, { level: 2 }) ) + .addBatch( + optimizerContext('level 2 off but duplicateMediaRemoving on', { + 'non-adjacent': [ + '@media screen{a{color:red}}@media print{a{color:#fff}}@media screen{a{color:red}}', + '@media print{a{color:#fff}}@media screen{a{color:red}}' + ] + }, { level: { 2: { all: false, duplicateMediaRemoving: true } } }) + ) .addBatch( optimizerContext('level 2 off', { 'keeps content same': [ @@ -33,8 +41,8 @@ vows.describe('remove duplicate media queries') .addBatch( optimizerContext('media merging off', { 'keeps content same': [ - '@media screen{a{color:red}}@media screen{a{color:red}}', - '@media screen{a{color:red}}@media screen{a{color:red}}' + '@media screen{a{color:red}}@media screen{div{color:red}}', + '@media screen{a{color:red}}@media screen{div{color:red}}' ] }, { level: { 2: { mediaMerging: false } } }) ) diff --git a/test/optimizer/level-2/remove-duplicates-test.js b/test/optimizer/level-2/remove-duplicates-test.js index cd3c42ba..4c84bdaa 100644 --- a/test/optimizer/level-2/remove-duplicates-test.js +++ b/test/optimizer/level-2/remove-duplicates-test.js @@ -42,6 +42,14 @@ vows.describe('remove duplicates') ] }, { level: 2 }) ) + .addBatch( + optimizerContext('level 2 off but removing duplicates on', { + 'same context': [ + 'a{color:red}div{color:blue}a{color:red}', + 'div{color:#00f}a{color:red}' + ] + }, { level: { 2: { all: false, duplicateRulesRemoving: true } } }) + ) .addBatch( optimizerContext('level 2 off', { 'same context': [ diff --git a/test/options/optimization-level-test.js b/test/options/optimization-level-test.js index 924ae131..33f26499 100644 --- a/test/options/optimization-level-test.js +++ b/test/options/optimization-level-test.js @@ -70,7 +70,13 @@ vows.describe(optimizationLevelFrom) }, 'has level 2 options': function (levelOptions) { assert.deepEqual(levelOptions['2'], { + adjacentRulesMerging: true, + duplicateFontRulesRemoving: true, + duplicateMediaRemoving: true, + duplicateRulesRemoving: true, mediaMerging: true, + nonAdjacentRulesMerging: true, + nonAdjacentRulesReducing: true, restructuring: false, semanticMerging: false, shorthandCompacting: true @@ -106,7 +112,13 @@ vows.describe(optimizationLevelFrom) }, 'has level 2 options': function (levelOptions) { assert.deepEqual(levelOptions['2'], { + adjacentRulesMerging: true, + duplicateFontRulesRemoving: true, + duplicateMediaRemoving: true, + duplicateRulesRemoving: true, mediaMerging: true, + nonAdjacentRulesMerging: true, + nonAdjacentRulesReducing: true, restructuring: false, semanticMerging: false, shorthandCompacting: true @@ -131,7 +143,13 @@ vows.describe(optimizationLevelFrom) }, 'has level 2 options': function (levelOptions) { assert.deepEqual(levelOptions['2'], { + adjacentRulesMerging: false, + duplicateFontRulesRemoving: false, + duplicateMediaRemoving: false, + duplicateRulesRemoving: false, mediaMerging: true, + nonAdjacentRulesMerging: false, + nonAdjacentRulesReducing: false, restructuring: false, semanticMerging: false, shorthandCompacting: false @@ -156,7 +174,13 @@ vows.describe(optimizationLevelFrom) }, 'has level 2 options': function (levelOptions) { assert.deepEqual(levelOptions['2'], { + adjacentRulesMerging: false, + duplicateFontRulesRemoving: false, + duplicateMediaRemoving: false, + duplicateRulesRemoving: false, mediaMerging: true, + nonAdjacentRulesMerging: false, + nonAdjacentRulesReducing: false, restructuring: false, semanticMerging: false, shorthandCompacting: false @@ -198,7 +222,13 @@ vows.describe(optimizationLevelFrom) }, 'has level 2 options': function (levelOptions) { assert.deepEqual(levelOptions['2'], { + adjacentRulesMerging: true, + duplicateFontRulesRemoving: true, + duplicateMediaRemoving: true, + duplicateRulesRemoving: true, mediaMerging: false, + nonAdjacentRulesMerging: true, + nonAdjacentRulesReducing: true, restructuring: false, semanticMerging: true, shorthandCompacting: true @@ -223,7 +253,13 @@ vows.describe(optimizationLevelFrom) }, 'has level 2 options': function (levelOptions) { assert.deepEqual(levelOptions['2'], { + adjacentRulesMerging: true, + duplicateFontRulesRemoving: true, + duplicateMediaRemoving: true, + duplicateRulesRemoving: true, mediaMerging: false, + nonAdjacentRulesMerging: true, + nonAdjacentRulesReducing: true, restructuring: false, semanticMerging: true, shorthandCompacting: true @@ -248,7 +284,13 @@ vows.describe(optimizationLevelFrom) }, 'has level 2 options': function (levelOptions) { assert.deepEqual(levelOptions['2'], { + adjacentRulesMerging: false, + duplicateFontRulesRemoving: false, + duplicateMediaRemoving: false, + duplicateRulesRemoving: false, mediaMerging: true, + nonAdjacentRulesMerging: false, + nonAdjacentRulesReducing: false, restructuring: false, semanticMerging: true, shorthandCompacting: false @@ -273,7 +315,13 @@ vows.describe(optimizationLevelFrom) }, 'has level 2 options': function (levelOptions) { assert.deepEqual(levelOptions['2'], { + adjacentRulesMerging: false, + duplicateFontRulesRemoving: false, + duplicateMediaRemoving: false, + duplicateRulesRemoving: false, mediaMerging: true, + nonAdjacentRulesMerging: false, + nonAdjacentRulesReducing: false, restructuring: false, semanticMerging: true, shorthandCompacting: false -- 2.34.1