From 2ff65ec8c90761baf9bf2585d774076df233f19b Mon Sep 17 00:00:00 2001 From: Jakub Pawlowicz Date: Tue, 6 Jan 2015 20:29:07 +0000 Subject: [PATCH] Fixes #158 - adds advanced selectors rearrangements. * Moves selectors around if moved properties are not redefined in between. --- History.md | 1 + lib/selectors/optimizers/advanced.js | 61 ++++++++++++++++++++++++++++ test/fixtures/big-min.css | 3 +- test/integration-test.js | 46 +++++++++++++-------- test/selectors/optimizer-test.js | 4 +- 5 files changed, 95 insertions(+), 20 deletions(-) diff --git a/History.md b/History.md index b5582fc4..35e42df1 100644 --- a/History.md +++ b/History.md @@ -3,6 +3,7 @@ * Adds 0deg to 0 minification where possible. * Adds better non-adjacent selector merging when body is the same. +* Fixed issue [#158](https://github.com/GoalSmashers/clean-css/issues/158) - adds body-based selectors reduction. * Fixed issue [#182](https://github.com/GoalSmashers/clean-css/issues/182) - removing space after closing brace. [3.0.2 / 2015-01-04](https://github.com/jakubpawlowicz/clean-css/compare/v3.0.1...v3.0.2) diff --git a/lib/selectors/optimizers/advanced.js b/lib/selectors/optimizers/advanced.js index 5b715696..7fe297bb 100644 --- a/lib/selectors/optimizers/advanced.js +++ b/lib/selectors/optimizers/advanced.js @@ -23,6 +23,24 @@ function unsafeSelector(value) { return /\.|\*| :/.test(value); } +function extractProperties(token) { + return (token.metadata.body.match(/([a-z\-]+):/g) || []) + .join('/') + .replace(/\-[a-z]+/g, '') + .split('/'); +} + +function unsafeTraversal(token, allProperties) { + var properties = extractProperties(token); + + for (var j = 0; j < properties.length; j++) { + if (allProperties.indexOf(properties[j]) > -1) + return true; + } + + return false; +} + AdvancedOptimizer.prototype.isSpecial = function (selector) { return this.options.compatibility.selectors.special.test(selector); }; @@ -259,6 +277,48 @@ AdvancedOptimizer.prototype.reduceSelector = function (tokens, selector, data, o } }; +AdvancedOptimizer.prototype.mergeNonAdjacentBySelector = function (tokens) { + var allSelectors = {}; + var repeatedSelectors = []; + var i; + + for (i = tokens.length - 1; i >= 0; i--) { + if (tokens[i].kind != 'selector') + continue; + if (tokens[i].body.length === 0) + continue; + + var selector = tokens[i].metadata.selector; + allSelectors[selector] = [i].concat(allSelectors[selector] || []); + + if (allSelectors[selector].length == 2) + repeatedSelectors.push(selector); + } + + for (i = repeatedSelectors.length - 1; i >= 0; i--) { + var positions = allSelectors[repeatedSelectors[i]]; + + selectorIterator: + for (var j = positions.length - 1; j > 0; j--) { + var targetPosition = positions[j - 1]; + var targetToken = tokens[targetPosition]; + var movedPosition = positions[j]; + var movedToken = tokens[movedPosition]; + var movedProperties = extractProperties(movedToken); + + for (var k = movedPosition - 1; k > targetPosition; k--) { + if (tokens[k].kind == 'selector' && unsafeTraversal(tokens[k], movedProperties)) + continue selectorIterator; + } + + var joinAt = [movedToken.body.length]; + var newBody = this.propertyOptimizer.process(targetToken.value, targetToken.body.concat(movedToken.body), joinAt, true); + changeBodyOf(targetToken, newBody); + movedToken.body = []; + } + } +}; + AdvancedOptimizer.prototype.mergeNonAdjacentByBody = function (tokens) { var candidates = {}; @@ -318,6 +378,7 @@ AdvancedOptimizer.prototype.optimize = function (tokens) { self.removeDuplicates(tokens); self.mergeAdjacent(tokens); + self.mergeNonAdjacentBySelector(tokens); self.mergeNonAdjacentByBody(tokens); } diff --git a/test/fixtures/big-min.css b/test/fixtures/big-min.css index fc8116b7..069a3217 100644 --- a/test/fixtures/big-min.css +++ b/test/fixtures/big-min.css @@ -750,10 +750,9 @@ img[height="97"]+.ico29x29{bottom:6%;left:3.5%} .portfolio_appel_revolutionnaire.conteneur_carrousel .navigation .suivant{left:auto;right:0} .portfolio_appel_revolutionnaire a .legende.bg_fonce{color:#fff} #barre_titre,#header,#nav{position:relative} -#header{z-index:3} +#header{z-index:3;font-size:12px;text-align:left} #barre-titre{z-index:2} #nav{z-index:1} -#header{font-size:12px;text-align:left} #header a{display:inline-block} .conteneur_haut{width:1000px;margin:0 auto} #surheader,#surheader .conteneur_haut{background:#1e5799;background:-moz-linear-gradient(top,#1e5799 0,#2d3841 0,#010c16 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0,#1e5799),color-stop(0,#2d3841),color-stop(100%,#010c16));background:-webkit-linear-gradient(top,#1e5799 0,#2d3841 0,#010c16 100%);background:-o-linear-gradient(top,#1e5799 0,#2d3841 0,#010c16 100%);background:-ms-linear-gradient(top,#1e5799 0,#2d3841 0,#010c16 100%);background:linear-gradient(top,#1e5799 0,#2d3841 0,#010c16 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2d3841', endColorstr='#010c16', GradientType=0);height:25px;line-height:25px} diff --git a/test/integration-test.js b/test/integration-test.js index 4886c505..92be1165 100644 --- a/test/integration-test.js +++ b/test/integration-test.js @@ -1112,9 +1112,9 @@ path")}', 'a{color:red\\9;display:block;color:#fff\\9}', 'a{display:block;color:#fff\\9}' ], - 'overriding a star by a non-ajacent selector': 'a{color:red}.one{display:block}a{*color:#fff}', - 'overriding a unserscore by a non-ajacent selector': 'a{color:red}.one{display:block}a{_color:#fff}', - 'overriding a backslash by a non-ajacent selector': 'a{color:red}.one{display:block}a{color:#fff\\9}', + 'overriding a star by a non-ajacent selector': 'a{color:red}.one{color:#fff}a{*color:#fff}', + 'overriding an underscore by a non-ajacent selector': 'a{color:red}.one{color:#fff}a{_color:#fff}', + 'overriding a backslash by a non-ajacent selector': 'a{color:red}.one{color:#fff}a{color:#fff\\9}', 'keeps rgba(0,0,0,0)': 'a{color:rgba(0,0,0,0)}', 'keeps rgba(255,255,255,0)': 'a{color:rgba(255,255,255,0)}', 'keeps hsla(120,100%,50%,0)': 'a{color:hsla(120,100%,50%,0)}' @@ -1670,7 +1670,10 @@ title']{display:block}", ] }, { aggressiveMerging: false }), 'same selectors': cssContext({ - 'of two non-adjacent selectors': '.one{color:red}.two{color:#00f}.one{font-weight:700}', + 'of two non-adjacent selectors': [ + '.one{color:red}.two{color:#00f}.one{font-weight:700}', + '.one{color:red;font-weight:700}.two{color:#00f}' + ], 'of two adjacent single selectors': [ '.one{color:red}.one{font-weight:700}', '.one{color:red;font-weight:700}' @@ -1710,18 +1713,18 @@ title']{display:block}", 'when overriden with a browser specific selector': 'a{color:red}::-webkit-scrollbar,a{color:#fff}' }), 'same non-adjacent selectors': cssContext({ - 'with different properties': 'a{color:red;display:block}.one{font-size:12px}a{margin:2px}', + 'with different properties': 'a{color:red;display:block}.one{margin:5px}a{margin:2px}', 'with one redefined property': [ - 'a{color:red;display:block}.one{font-size:12px}a{color:#fff;margin:2px}', - 'a{display:block}.one{font-size:12px}a{color:#fff;margin:2px}' + 'a{color:red;display:block}.one{color:red}a{color:#fff;margin:2px}', + 'a{display:block}.one{color:red}a{color:#fff;margin:2px}' ], 'with intentionally redefined properties on joins': [ - 'a{display:inline-block;display:-moz-inline-box;color:red}.one{font-size:12px}a{color:#fff;margin:2px}', - 'a{display:inline-block;display:-moz-inline-box}.one{font-size:12px}a{color:#fff;margin:2px}' + 'a{display:inline-block;display:-moz-inline-box;color:red}.one{margin:12px}a{color:#fff;margin:2px}', + 'a{display:inline-block;display:-moz-inline-box}.one{margin:12px}a{color:#fff;margin:2px}' ], - 'with intentionally redefined properties on nultiple joins': [ - 'a{color:red}.one{font-size:12px}a{color:#fff;margin:2px}.two{font-weight:400}a{margin:0}', - '.one{font-size:12px}a{color:#fff}.two{font-weight:400}a{margin:0}' + 'with intentionally redefined properties on multiple joins': [ + 'a{color:red}.one{font-size:12px}a{color:#fff;margin:2px}.two{margin:10px}a{margin:0}', + '.one{font-size:12px}a{color:#fff}.two{margin:10px}a{margin:0}' ], 'with all redefined properties': [ 'a{color:red;display:block}.one{font-size:12px}a{color:#fff;display:inline-block;margin:2px}', @@ -1744,16 +1747,15 @@ title']{display:block}", 'a{margin:0}a,p{color:red;padding:0}.one,a{color:#fff}' ], 'when complex selector overriden by simple selectors': 'a,p{margin:0;color:red}a{color:#fff}', - // Pending re-run selectors merge - see #160 'when complex selector overriden by complex and simple selectors': [ 'a,p{margin:0;color:red}a{color:#fff}a,p{color:#00f}p{color:#0f0}', - 'a,p{margin:0}a,p{color:#00f}p{color:#0f0}' + 'a,p{margin:0;color:#00f}p{color:#0f0}' ], 'when complex selector overriden by complex selectors': [ '.one>.two,.three{color:red;line-height:1rem}#zero,.one>.two,.three,.www{color:#fff;margin:0}a{color:red}.one>.two,.three{line-height:2rem;font-size:1.5rem}', '#zero,.one>.two,.three,.www{color:#fff;margin:0}a{color:red}.one>.two,.three{line-height:2rem;font-size:1.5rem}' ], - 'when undefined is used as a value': '.one{text-shadow:undefined}p{color:red}.one{font-size:12px}', + 'when undefined is used as a value': '.one{text-shadow:undefined}p{font-size:14px}.one{font-size:12px}', 'when undefined is used as a value with reduction': [ '.one{text-shadow:undefined}p{color:red}.one{font-size:12px;text-shadow:none}', 'p{color:red}.one{font-size:12px;text-shadow:none}' @@ -1763,7 +1765,19 @@ title']{display:block}", 'a,::-moz-selection{color:red}p{display:block}a,::-moz-selection{color:#fff}', 'p{display:block}::-moz-selection,a{color:#fff}' ], - 'with full property comparison': '.one{height:7rem}.two{height:auto}.one{line-height:7rem;color:red}' + 'with full property comparison': '.one{height:7rem}.two{color:#fff}.one{line-height:7rem;color:red}', + 'with two intermediate, non-overriding selectors': [ + '.one{color:red;margin:0}.two{color:#fff}.one{font-size:12px}', + '.one{color:red;margin:0;font-size:12px}.two{color:#fff}' + ], + 'with two intermediate, overriding more specific selectors': [ + '.one{color:red;margin:0}.two{font:12px serif}.one{font-size:12px}', + '.one{color:red;margin:0}.two{font:12px serif}.one{font-size:12px}' + ], + 'with three intermediate, non-overriding selectors': [ + '.one{color:red;margin:0}.two{color:#fff}.one{font-size:12px}.three{color:#000}.one{padding:0}', + '.one{color:red;margin:0;font-size:12px;padding:0}.two{color:#fff}.three{color:#000}' + ] }), 'rerun optimizers': cssContext({ 'selectors reducible once': [ diff --git a/test/selectors/optimizer-test.js b/test/selectors/optimizer-test.js index e4185d98..4bc43a4d 100644 --- a/test/selectors/optimizer-test.js +++ b/test/selectors/optimizer-test.js @@ -148,8 +148,8 @@ vows.describe(SelectorsOptimizer) 'a{color:red;display:block;width:100px}div{color:#fff}' ], 'non-adjacent': [ - 'a{color:red;display:block}.one{font-size:12px}a{color:#fff;margin:2px}', - 'a{display:block}.one{font-size:12px}a{color:#fff;margin:2px}' + 'a{color:red;display:block}.one{margin:12px}a{color:#fff;margin:2px}', + 'a{display:block}.one{margin:12px}a{color:#fff;margin:2px}' ], 'non-adjacent with multi selectors': [ 'a{padding:10px;margin:0;color:red}.one{color:red}a,p{color:red;padding:0}', -- 2.34.1