From: GoalSmashers Date: Thu, 31 Oct 2013 08:16:26 +0000 (+0100) Subject: Adds property overriding so more coarse properties override more granular ones. X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=1f71d7bcab1b66f301c74249a7701588d3dc8edd;p=clean-css.git Adds property overriding so more coarse properties override more granular ones. * E.g. 'border-top' overrides 'border-top-width' and 'border-top-color'. * Another example is 'margin' overriding 'margin-(bottom|left|right|top)'. --- diff --git a/History.md b/History.md index 7485d561..75fd2e3f 100644 --- a/History.md +++ b/History.md @@ -13,6 +13,7 @@ * Adds merging adjacent selectors within a scope (single and multiple ones). * Changes behavior of `--keep-line-breaks`/`keepBreaks` option to keep breaks after trailing braces only. * Makes all multiple selectors ordered alphabetically (aids merging). +* Adds property overriding so more coarse properties override more granular ones. 1.1.7 / 2013-10-28 ================== diff --git a/lib/properties/optimizer.js b/lib/properties/optimizer.js index 2b1bc55b..5c3b15a4 100644 --- a/lib/properties/optimizer.js +++ b/lib/properties/optimizer.js @@ -1,4 +1,114 @@ module.exports = function Optimizer() { + var overridable = { + 'animation-delay': ['animation'], + 'animation-direction': ['animation'], + 'animation-duration': ['animation'], + 'animation-fill-mode': ['animation'], + 'animation-iteration-count': ['animation'], + 'animation-name': ['animation'], + 'animation-play-state': ['animation'], + 'animation-timing-function': ['animation'], + '-moz-animation-delay': ['-moz-animation'], + '-moz-animation-direction': ['-moz-animation'], + '-moz-animation-duration': ['-moz-animation'], + '-moz-animation-fill-mode': ['-moz-animation'], + '-moz-animation-iteration-count': ['-moz-animation'], + '-moz-animation-name': ['-moz-animation'], + '-moz-animation-play-state': ['-moz-animation'], + '-moz-animation-timing-function': ['-moz-animation'], + '-o-animation-delay': ['-o-animation'], + '-o-animation-direction': ['-o-animation'], + '-o-animation-duration': ['-o-animation'], + '-o-animation-fill-mode': ['-o-animation'], + '-o-animation-iteration-count': ['-o-animation'], + '-o-animation-name': ['-o-animation'], + '-o-animation-play-state': ['-o-animation'], + '-o-animation-timing-function': ['-o-animation'], + '-webkit-animation-delay': ['-webkit-animation'], + '-webkit-animation-direction': ['-webkit-animation'], + '-webkit-animation-duration': ['-webkit-animation'], + '-webkit-animation-fill-mode': ['-webkit-animation'], + '-webkit-animation-iteration-count': ['-webkit-animation'], + '-webkit-animation-name': ['-webkit-animation'], + '-webkit-animation-play-state': ['-webkit-animation'], + '-webkit-animation-timing-function': ['-webkit-animation'], + 'background-attachment': ['background'], + 'background-clip': ['background'], + 'background-color': ['background'], + 'background-image': ['background'], + 'background-origin': ['background'], + 'background-position': ['background'], + 'background-repeat': ['background'], + 'background-size': ['background'], + 'border-color': ['border'], + 'border-style': ['border'], + 'border-width': ['border'], + 'border-bottom': ['border'], + 'border-bottom-color': ['border-bottom', 'border-color', 'border'], + 'border-bottom-style': ['border-bottom', 'border-style', 'border'], + 'border-bottom-width': ['border-bottom', 'border-width', 'border'], + 'border-left': ['border'], + 'border-left-color': ['border-left', 'border-color', 'border'], + 'border-left-style': ['border-left', 'border-style', 'border'], + 'border-left-width': ['border-left', 'border-width', 'border'], + 'border-right': ['border'], + 'border-right-color': ['border-right', 'border-color', 'border'], + 'border-right-style': ['border-right', 'border-style', 'border'], + 'border-right-width': ['border-right', 'border-width', 'border'], + 'border-top': ['border'], + 'border-top-color': ['border-top', 'border-color', 'border'], + 'border-top-style': ['border-top', 'border-style', 'border'], + 'border-top-width': ['border-top', 'border-width', 'border'], + 'font-family': ['font'], + 'font-size': ['font'], + 'font-style': ['font'], + 'font-variant': ['font'], + 'font-weight': ['font'], + 'list-style-image': ['list'], + 'list-style-position': ['list'], + 'list-style-type': ['list'], + 'margin-bottom': ['margin'], + 'margin-left': ['margin'], + 'margin-right': ['margin'], + 'margin-top': ['margin'], + 'outline-color': ['outline'], + 'outline-style': ['outline'], + 'outline-width': ['outline'], + 'padding-bottom': ['padding'], + 'padding-left': ['padding'], + 'padding-right': ['padding'], + 'padding-top': ['padding'], + 'transition-delay': ['transition'], + 'transition-duration': ['transition'], + 'transition-property': ['transition'], + 'transition-timing-function': ['transition'], + '-moz-transition-delay': ['-moz-transition'], + '-moz-transition-duration': ['-moz-transition'], + '-moz-transition-property': ['-moz-transition'], + '-moz-transition-timing-function': ['-moz-transition'], + '-o-transition-delay': ['-o-transition'], + '-o-transition-duration': ['-o-transition'], + '-o-transition-property': ['-o-transition'], + '-o-transition-timing-function': ['-o-transition'], + '-webkit-transition-delay': ['-webkit-transition'], + '-webkit-transition-duration': ['-webkit-transition'], + '-webkit-transition-property': ['-webkit-transition'], + '-webkit-transition-timing-function': ['-webkit-transition'] + }; + + var overrides = {}; + for (var granular in overridable) { + for (var i = 0; i < overridable[granular].length; i++) { + var coarse = overridable[granular][i]; + var list = overrides[coarse]; + + if (list) + list.push(granular); + else + overrides[coarse] = [granular]; + } + } + var tokenize = function(body) { var tokens = body.split(';'); var keyValues = []; @@ -6,47 +116,80 @@ module.exports = function Optimizer() { for (var i = 0, l = tokens.length; i < l; i++) { var token = tokens[i]; var firstColon = token.indexOf(':'); - keyValues.push([token.substring(0, firstColon), token.substring(firstColon + 1)]); + keyValues.push([ + token.substring(0, firstColon), + token.substring(firstColon + 1), + token.indexOf('!important') > -1 + ]); } return keyValues; }; - var optimize = function(tokenized, allowAdjacent) { + var optimize = function(tokens, allowAdjacent) { var merged = []; var properties = []; var lastProperty = null; + var rescanTrigger = {}; + + var removeOverridenBy = function(property, isImportant) { + var overrided = overrides[property]; + for (var i = 0, l = overrided.length; i < l; i++) { + for (var j = 0; j < properties.length; j++) { + if (properties[j] != overrided[i] || (merged[j][2] && !isImportant)) + continue; - for (var i = 0, l = tokenized.length; i < l; i++) { - var property = tokenized[i][0]; - var value = tokenized[i][1]; - var alreadyIn = properties.indexOf(property); + merged.splice(j, 1); + properties.splice(j, 1); + j -= 1; + } + } + }; - if (alreadyIn > -1 && merged[alreadyIn][1].indexOf('!important') > 0 && value.indexOf('!important') == -1) + for (var i = 0, l = tokens.length; i < l; i++) { + var token = tokens[i]; + var property = token[0]; + var isImportant = token[2]; + var alreadyOnPosition = properties.indexOf(property); + + if (alreadyOnPosition > -1 && merged[alreadyOnPosition][2] && !isImportant) continue; // comment is necessary - we assume that if two properties are one after another // then it is intentional way of redefining property which may not be widely supported - // however if `allowAdjacent` is set then the rule does not apply (see merging two adjacent selectors) - if (alreadyIn > -1 && (allowAdjacent || lastProperty != property)) { - merged.splice(alreadyIn, 1); - properties.splice(alreadyIn, 1); + // e.g. a{display:inline-block;display:-moz-inline-box} + // however if `allowAdjacent` is set then the rule does not apply + // (e.g merging two adjacent selectors) + if (alreadyOnPosition > -1 && (allowAdjacent || lastProperty != property)) { + merged.splice(alreadyOnPosition, 1); + properties.splice(alreadyOnPosition, 1); } - merged.push([property, value]); + merged.push(token); properties.push(property); + // certain properties (see values of `overridable`) should trigger removal of + // more granular properties (see keys of `overridable`) + if (rescanTrigger[property]) + removeOverridenBy(property, isImportant); + + // add rescan triggers - if certain property appears later in the list a rescan needs + // to be triggered, e.g 'border-top' triggers a rescan after 'border-top-width' and + // 'border-top-color' as they can be removed + for (var j = 0, list = overridable[property] || [], m = list.length; j < m; j++) + rescanTrigger[list[j]] = true; + lastProperty = property; } return merged; }; - var rebuild = function(tokenized) { + var rebuild = function(tokens) { var flat = []; - for (var i = 0, l = tokenized.length; i < l; i++) { - flat.push(tokenized[i][0] + ':' + tokenized[i][1]); + for (var i = 0, l = tokens.length; i < l; i++) { + flat.push(tokens[i][0] + ':' + tokens[i][1]); } return flat.join(';'); @@ -54,11 +197,11 @@ module.exports = function Optimizer() { return { process: function(body, allowAdjacent) { - var tokenized = tokenize(body); - if (tokenized.length < 2) + var tokens = tokenize(body); + if (tokens.length < 2) return body; - var optimized = optimize(tokenized, allowAdjacent); + var optimized = optimize(tokens, allowAdjacent); return rebuild(optimized); } }; diff --git a/test/data/big-min.css b/test/data/big-min.css index 273e8d32..77345d75 100644 --- a/test/data/big-min.css +++ b/test/data/big-min.css @@ -251,7 +251,7 @@ section article{margin:0 0 16px} .bloc_abo{border-top:3px solid #ffd500} img[width="642"],img[width="312"]{margin-bottom:6px} img[width="202"]{margin-bottom:4px} -.btn,.btn_abo,.btn_fonce,.btn_petit{display:inline-block;padding:4px 10px;margin-bottom:0;color:#000b15;text-align:center;font-weight:700;vertical-align:middle;background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-ms-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(top,#fff,#e6e6e6);background-repeat:repeat-x;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);border:1px solid #ccc;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);cursor:pointer} +.btn,.btn_abo,.btn_fonce,.btn_petit{display:inline-block;padding:4px 10px;margin-bottom:0;color:#000b15;text-align:center;font-weight:700;vertical-align:middle;background-color:#f5f5f5;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-ms-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(top,#fff,#e6e6e6);background-repeat:repeat-x;filter:progid:dximagetransform.microsoft.gradient(enabled=false);border:1px solid #ccc;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);cursor:pointer} .bt_fonce a,.btn_fonce{color:#fff;background-color:#000b15;background-image:-moz-linear-gradient(top,#5d666d,#000b15);background-image:-ms-linear-gradient(top,#5d666d,#000b15);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5d666d),to(#000b15));background-image:-webkit-linear-gradient(top,#5d666d,#000b15);background-image:-o-linear-gradient(top,#5d666d,#000b15);background-image:linear-gradient(top,#5d666d,#000b15);background-repeat:repeat-x;border-color:#000b15;border-color:rgba(0,0,0,.1);filter:progid:dximagetransform.microsoft.gradient(enabled=false)} .btn_abo{color:#000b15;background-color:#ffc600;background-image:-moz-linear-gradient(top,#ffe562,#ffc600);background-image:-ms-linear-gradient(top,#ffe562,#ffc600);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ffe562),to(#ffc600));background-image:-webkit-linear-gradient(top,#ffe562,#ffc600);background-image:-o-linear-gradient(top,#ffe562,#ffc600);background-image:linear-gradient(top,#ffe562,#ffc600);background-repeat:repeat-x;border-color:#ffc600;border-color:rgba(0,0,0,.1);filter:progid:dximagetransform.microsoft.gradient(enabled=false)} .btn.large{width:100%;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box} @@ -886,7 +886,7 @@ label i{font-style:normal;display:none} #nav{clear:both;height:32px;margin:0 auto;width:1000px;background:#fafbfc;border-top:3px solid transparent;border-bottom:1px solid #dddee0} #nav .conteneur_bordure{width:998px;margin:-3px auto 0;border-top:3px solid #ffd500;border-left:1px solid #d2d6db;border-right:1px solid #d2d6db} #nav.accueil{width:auto;border-top:solid 3px #2E3942;background:#fff} -#nav ul{margin-top:-3px;overflow:hidden;width:1000px;margin:-3px auto 0} +#nav ul{overflow:hidden;width:1000px;margin:-3px auto 0} #nav.acceuil ul{width:998px} #nav li{display:block;float:left;border-top-width:3px;border-top-style:solid} #nav a,#nav span{display:inline-block;height:25px;padding:7px 10px 0 9px;border-left:1px solid #d2d6db;border-bottom:1px solid #d2d6db;font-size:12px;font-weight:700;text-transform:uppercase;color:#000} diff --git a/test/unit-test.js b/test/unit-test.js index 527387ff..a91f5434 100644 --- a/test/unit-test.js +++ b/test/unit-test.js @@ -55,6 +55,32 @@ var colorShorteningContext = function() { return cssContext(shortenerContext); }; +var redefineContext = function(redefinitions, options) { + var context = {}; + var vendorPrefixes = ['', '-moz-', '-o-', '-webkit-']; // there is no -ms-animation nor -ms-transition. + + for (var property in redefinitions) { + for (var i = 0; i < redefinitions[property].length; i++) { + var by = redefinitions[property][i]; + var prefixes = options.vendorPrefixes.indexOf(by) > -1 ? vendorPrefixes : ['']; + + for (var j = 0, m = prefixes.length; j < m; j++) { + var prefixedProperty = prefixes[j] + property; + var prefixedBy = prefixes[j] + by; + + context['should override ' + prefixedProperty + ' by ' + prefixedBy] = [ + 'a{' + prefixedProperty + ':inherit;' + prefixedBy + ':0}', + 'a{' + prefixedBy + ':0}' + ]; + context['should not override ' + prefixedBy + ' by ' + prefixedProperty] = + 'a{' + prefixedBy + ':0;' + prefixedProperty + ':inherit}'; + } + } + } + + return cssContext(context); +}; + vows.describe('clean-units').addBatch({ 'identity': cssContext({ 'preserve minified content': 'a{color:#f10}' @@ -616,8 +642,8 @@ vows.describe('clean-units').addBatch({ 'font:700 .9rem Helvetica' ], 'multiple changes': [ - 'p{font-weight:bold;width:100%;font:normal 12px Helvetica}', - 'p{font-weight:700;width:100%;font:400 12px Helvetica}' + 'p{font-weight:bold!important;width:100%;font:normal 12px Helvetica}', + 'p{font-weight:700!important;width:100%;font:400 12px Helvetica}' ], 'font weight in extended font declarations': 'font:normal normal normal 13px/20px Helvetica' }), @@ -1168,5 +1194,82 @@ title']{display:block}", ], 'of supported and unsupported selector': '.one:first-child{color:red}.two:last-child{color:red}', 'of two unsupported selectors': '.one:nth-child(5){color:red}.two:last-child{color:red}' - }, { selectorsMergeMode: 'ie8' }) + }, { selectorsMergeMode: 'ie8' }), + 'redefined more granular properties': redefineContext({ + 'animation-delay': ['animation'], + 'animation-direction': ['animation'], + 'animation-duration': ['animation'], + 'animation-fill-mode': ['animation'], + 'animation-iteration-count': ['animation'], + 'animation-name': ['animation'], + 'animation-play-state': ['animation'], + 'animation-timing-function': ['animation'], + 'background-attachment': ['background'], + 'background-clip': ['background'], + 'background-color': ['background'], + 'background-image': ['background'], + 'background-origin': ['background'], + 'background-position': ['background'], + 'background-repeat': ['background'], + 'background-size': ['background'], + 'border-color': ['border'], + 'border-style': ['border'], + 'border-width': ['border'], + 'border-bottom': ['border'], + 'border-bottom-color': ['border-bottom', 'border-color', 'border'], + 'border-bottom-style': ['border-bottom', 'border-style', 'border'], + 'border-bottom-width': ['border-bottom', 'border-width', 'border'], + 'border-left': ['border'], + 'border-left-color': ['border-left', 'border-color', 'border'], + 'border-left-style': ['border-left', 'border-style', 'border'], + 'border-left-width': ['border-left', 'border-width', 'border'], + 'border-right': ['border'], + 'border-right-color': ['border-right', 'border-color', 'border'], + 'border-right-style': ['border-right', 'border-style', 'border'], + 'border-right-width': ['border-right', 'border-width', 'border'], + 'border-top': ['border'], + 'border-top-color': ['border-top', 'border-color', 'border'], + 'border-top-style': ['border-top', 'border-style', 'border'], + 'border-top-width': ['border-top', 'border-width', 'border'], + 'font-family': ['font'], + 'font-size': ['font'], + 'font-style': ['font'], + 'font-variant': ['font'], + 'font-weight': ['font'], + 'list-style-image': ['list'], + 'list-style-position': ['list'], + 'list-style-type': ['list'], + 'margin-bottom': ['margin'], + 'margin-left': ['margin'], + 'margin-right': ['margin'], + 'margin-top': ['margin'], + 'outline-color': ['outline'], + 'outline-style': ['outline'], + 'outline-width': ['outline'], + 'padding-bottom': ['padding'], + 'padding-left': ['padding'], + 'padding-right': ['padding'], + 'padding-top': ['padding'], + 'transition-delay': ['transition'], + 'transition-duration': ['transition'], + 'transition-property': ['transition'], + 'transition-timing-function': ['transition'] + }, { vendorPrefixes: ['animation', 'transition'] }), + 'complex granular properties': cssContext({ + 'two granular properties': 'a{border-bottom:1px solid red;border-color:red}', + 'two same granular properties': 'a{border-color:rgba(0,0,0,.5);border-color:red}', + 'two same granular properties redefined': [ + 'a{border-color:rgba(0,0,0,.5);border-color:red;border:0}', + 'a{border:0}' + ], + 'important granular property redefined': 'a{border-color:red!important;border:0}', + 'important granular property redefined with important': [ + 'a{border-color:red!important;border:0!important}', + 'a{border:0!important}' + ], + 'mix of border properties': [ + 'a{border-top:1px solid red;border-top-color:#0f0;color:red;border-top-width:2px;border-bottom-width:1px;border:0;border-left:1px solid red}', + 'a{color:red;border:0;border-left:1px solid red}' + ] + }) }).export(module);