Adds semantic merging (off by default).
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Mon, 4 May 2015 14:54:45 +0000 (15:54 +0100)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Sun, 31 May 2015 13:47:47 +0000 (14:47 +0100)
The main obstacle on reordering and merging declarations is a fact that
arbitrary classes can be applied to an element and at CSS level we don't
know if that's the case or not.

However with semantic merging mode on we trust CSS author knows what she is doing.

This commit is just a start (see #588) of a journey. It is and will always
be turned off by default as it requires certain effort from stylesheets'
author.

So far plain class selectors and some BEM basics are supported.

README.md
bin/cleancss
lib/clean.js
lib/selectors/optimizers/advanced.js
test/binary-test.js
test/module-test.js
test/selectors/optimizer-test.js

index 1c45481..97b4fc0 100644 (file)
--- a/README.md
+++ b/README.md
@@ -66,6 +66,7 @@ cleancss [options] source-file, [source-file, ...]
 -c, --compatibility [ie7|ie8]   Force compatibility mode (see Readme for advanced examples)
 --source-map                    Enables building input's source map
 --source-map-inline-sources     Enables inlining sources inside source map's `sourcesContent` field
+--semantic-merging              Enables semantic merging mode by assuming BEM-like content (warning, this may break your styling!)
 -d, --debug                     Shows debug information (minification time & compression efficiency)
 ```
 
@@ -128,6 +129,7 @@ CleanCSS constructor accepts a hash as a parameter, i.e.,
 * `restructuring` - set to false to disable restructuring in advanced optimizations
 * `root` - path to **resolve** absolute `@import` rules and **rebase** relative URLs
 * `roundingPrecision` - rounding precision; defaults to `2`; `-1` disables rounding
+* `semanticMerging` - set to true to enable semantic merging mode which assumes BEM-like content (default is false as it's highly likely this will break your stylesheets - **use with caution**!)
 * `shorthandCompacting` - set to false to skip shorthand compacting (default is true unless sourceMap is set when it's false)
 * `sourceMap` - exposes source map under `sourceMap` property, e.g. `new CleanCSS().minify(source).sourceMap` (default is false)
   If input styles are a product of CSS preprocessor (LESS, SASS) an input source map can be passed as a string.
index 5898319..bb22462 100755 (executable)
@@ -31,6 +31,7 @@ commands
   .option('-c, --compatibility [ie7|ie8]', 'Force compatibility mode (see Readme for advanced examples)')
   .option('--source-map', 'Enables building input\'s source map')
   .option('--source-map-inline-sources', 'Enables inlining sources inside source maps')
+  .option('--semantic-merging', 'Enables unsafe mode by assuming BEM-like semantic stylesheets (warning, this may break your styling!)')
   .option('-t, --timeout [seconds]', 'Per connection timeout when fetching remote @imports (defaults to 5 seconds)')
   .option('-d, --debug', 'Shows debug information (minification time & compression efficiency)');
 
@@ -72,6 +73,7 @@ var options = {
   restructuring: commands.skipRestructuring ? false : true,
   root: commands.root,
   roundingPrecision: commands.roundingPrecision,
+  semanticMerging: commands.semanticMerging ? true : false,
   shorthandCompacting: commands.skipShorthandCompacting ? false : true,
   sourceMap: commands.sourceMap,
   sourceMapInlineSources: commands.sourceMapInlineSources,
index 5347099..9f1e766 100644 (file)
@@ -49,6 +49,7 @@ var CleanCSS = module.exports = function CleanCSS(options) {
     restructuring: undefined === options.restructuring ? true : !!options.restructuring,
     root: options.root || process.cwd(),
     roundingPrecision: options.roundingPrecision,
+    semanticMerging: undefined === options.semanticMerging ? false : !!options.semanticMerging,
     shorthandCompacting: undefined === options.shorthandCompacting ? true : !!options.shorthandCompacting,
     sourceMap: options.sourceMap,
     sourceMapInlineSources: !!options.sourceMapInlineSources,
index 89ce48c..f4f9f51 100644 (file)
@@ -312,6 +312,27 @@ AdvancedOptimizer.prototype.mergeNonAdjacentBySelector = function (tokens) {
   }
 };
 
+function isBemElement(token) {
+  var asString = stringifySelectors(token[1]);
+  return asString.indexOf('__') > -1 || asString.indexOf('--') > -1;
+}
+
+function withoutModifier(selector) {
+  return selector.replace(/--[^ ,>\+~:]+/g, '');
+}
+
+function removeAnyUnsafeElements(left, candidates) {
+  var leftSelector = withoutModifier(stringifySelectors(left[1]));
+
+  for (var body in candidates) {
+    var right = candidates[body];
+    var rightSelector = withoutModifier(stringifySelectors(right[1]));
+
+    if (rightSelector.indexOf(leftSelector) > -1 || leftSelector.indexOf(rightSelector) > -1)
+      delete candidates[body];
+  }
+}
+
 AdvancedOptimizer.prototype.mergeNonAdjacentByBody = function (tokens) {
   var candidates = {};
   var adjacentSpace = this.options.compatibility.selectors.adjacentSpace;
@@ -321,9 +342,12 @@ AdvancedOptimizer.prototype.mergeNonAdjacentByBody = function (tokens) {
     if (token[0] != 'selector')
       continue;
 
-    if (token[2].length > 0 && unsafeSelector(stringifySelectors(token[1])))
+    if (token[2].length > 0 && (!this.options.semanticMerging && unsafeSelector(stringifySelectors(token[1]))))
       candidates = {};
 
+    if (token[2].length > 0 && this.options.semanticMerging && isBemElement(token))
+      removeAnyUnsafeElements(token, candidates);
+
     var oldToken = candidates[stringifyBody(token[2])];
     if (oldToken && !this.isSpecial(stringifySelectors(token[1])) && !this.isSpecial(stringifySelectors(oldToken[1]))) {
       token[1] = CleanUp.selectors(oldToken[1].concat(token[1]), false, adjacentSpace);
index 415f87b..88ab37b 100644 (file)
@@ -498,5 +498,17 @@ exports.commandsSuite = vows.describe('binary commands').addBatch({
         deleteFile('import-inline.min.css.map');
       }
     })
+  },
+  'semantic merging': {
+    'disabled': pipedContext('.a{margin:0}.b{margin:10px;padding:0}.c{margin:0}', '', {
+      'should output right data': function(error, stdout) {
+        assert.equal(stdout, '.a{margin:0}.b{margin:10px;padding:0}.c{margin:0}');
+      }
+    }),
+    'enabled': pipedContext('.a{margin:0}.b{margin:10px;padding:0}.c{margin:0}', '--semantic-merging', {
+      'should output right data': function(error, stdout) {
+        assert.equal(stdout, '.a,.c{margin:0}.b{margin:10px;padding:0}');
+      }
+    })
   }
 });
index 2c1f64d..5bdd7c4 100644 (file)
@@ -364,6 +364,22 @@ vows.describe('module tests').addBatch({
       'gets right output': function (minified) {
         assert.equal(minified.styles, 'div{margin-top:0}.one{margin:0}.two{display:block;margin-top:0}');
       }
+    },
+    'semantic merging - off': {
+      'topic': function () {
+        return new CleanCSS().minify('.a{margin:0}.b{margin:10px;padding:0}.c{margin:0}');
+      },
+      'gets right output': function (minified) {
+        assert.equal(minified.styles, '.a{margin:0}.b{margin:10px;padding:0}.c{margin:0}');
+      }
+    },
+    'semantic merging - on': {
+      'topic': function () {
+        return new CleanCSS({ semanticMerging: true }).minify('.a{margin:0}.b{margin:10px;padding:0}.c{margin:0}');
+      },
+      'gets right output': function (minified) {
+        assert.equal(minified.styles, '.a,.c{margin:0}.b{margin:10px;padding:0}');
+      }
     }
   },
   'source map': {
index 4c5a9e3..d2c2c08 100644 (file)
@@ -235,6 +235,43 @@ vows.describe(SelectorsOptimizer)
       ]
     }, { advanced: true })
   )
+  .addBatch(
+    optimizerContext('selectors - semantic merging mode', {
+      'simple': [
+        '.a{color:red}.b{color:#000}.c{color:red}',
+        '.a,.c{color:red}.b{color:#000}'
+      ],
+      'BEM - modifiers #1': [
+        '.block{color:red}.block__element{color:#000}.block__element--modifier{color:red}',
+        '.block{color:red}.block__element{color:#000}.block__element--modifier{color:red}'
+      ],
+      'BEM - modifiers #2': [
+        '.block1{color:red}.block1__element,.block2{color:#000}.block1__element--modifier{color:red}',
+        '.block1{color:red}.block1__element,.block2{color:#000}.block1__element--modifier{color:red}'
+      ],
+      'BEM - modifiers #3': [
+        '.block1{color:red}.block1--modifier,.block2{color:#000}.block1--another-modifier{color:red}',
+        '.block1{color:red}.block1--modifier,.block2{color:#000}.block1--another-modifier{color:red}'
+      ],
+      'BEM - tail merging': [
+        '.block1{color:red}.block1__element{color:#000}.block1__element--modifier{color:red}a{color:red}.block2__element--modifier{color:red}',
+        '.block1{color:red}.block1__element{color:#000}.block1__element--modifier,.block2__element--modifier,a{color:red}'
+      ],
+      'BEM - two blocks #1': [
+        '.block1__element{color:#000}.block2{color:red}.block2__element{color:#000}.block2__element--modifier{color:red}',
+        '.block1__element,.block2__element{color:#000}.block2,.block2__element--modifier{color:red}'
+      ],
+      'BEM - two blocks #2': [
+        '.block1__element{color:#000}.block1__element--modifier{color:red}.block2{color:red}.block2__element{color:#000}.block2__element--modifier{color:red}',
+        '.block1__element,.block2__element{color:#000}.block1__element--modifier,.block2,.block2__element--modifier{color:red}'
+      ],
+      'BEM - complex traversing #1': [
+        '.block1__element{color:#000}.block1__element--modifier{color:red}.block2{color:#000;display:block;width:100%}',
+        '.block1__element{color:#000}.block1__element--modifier{color:red}.block2{color:#000;display:block;width:100%}'
+        // '.block1__element,.block2{color:#000}.block1__element--modifier{color:red}.block2{display:block;width:100%}' - pending #588
+      ]
+    }, { advanced: true, semanticMerging: true })
+  )
   .addBatch(
     optimizerContext('properties', {
       'empty body': [