Adds property overriding so more coarse properties override more granular ones.
authorGoalSmashers <jakub@goalsmashers.com>
Thu, 31 Oct 2013 08:16:26 +0000 (09:16 +0100)
committerGoalSmashers <jakub@goalsmashers.com>
Sun, 3 Nov 2013 08:49:23 +0000 (09:49 +0100)
* E.g. 'border-top' overrides 'border-top-width' and 'border-top-color'.
* Another example is 'margin' overriding 'margin-(bottom|left|right|top)'.

History.md
lib/properties/optimizer.js
test/data/big-min.css
test/unit-test.js

index 7485d56..75fd2e3 100644 (file)
@@ -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
 ==================
index 2b1bc55..5c3b15a 100644 (file)
@@ -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);
     }
   };
index 273e8d3..77345d7 100644 (file)
@@ -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}
index 527387f..a91f543 100644 (file)
@@ -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);