Fixes #796 - enables semantic merging for `@media` blocks.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Tue, 3 Jan 2017 16:37:56 +0000 (17:37 +0100)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Wed, 4 Jan 2017 17:04:13 +0000 (18:04 +0100)
* It's off by default and can be enabled via `semanticMerging` flag;
* Handles all cases and BEM classes.

lib/optimizer/merge-media-queries.js
lib/optimizer/rules-overlap.js [new file with mode: 0644]
test/optimizer/merge-media-queries-test.js
test/optimizer/rules-overlap-test.js [new file with mode: 0644]

index 711dfa2..a8adbad 100644 (file)
@@ -1,10 +1,13 @@
 var canReorder = require('./reorderable').canReorder;
+var canReorderSingle = require('./reorderable').canReorderSingle;
 var extractProperties = require('./extract-properties');
+var rulesOverlap = require('./rules-overlap');
 
 var serializeRules = require('../writer/one-time').rules;
 var Token = require('../tokenizer/token');
 
-function mergeMediaQueries(tokens) {
+function mergeMediaQueries(tokens, context) {
+  var semanticMerging = context.options.semanticMerging;
   var candidates = {};
   var reduced = [];
 
@@ -48,6 +51,10 @@ function mergeMediaQueries(tokens) {
           var traversedProperties = extractProperties(tokens[from]);
           from += delta;
 
+          if (semanticMerging && allSameRulePropertiesCanBeReordered(movedProperties, traversedProperties)) {
+            continue;
+          }
+
           if (!canReorder(movedProperties, traversedProperties))
             continue directionLoop;
         }
@@ -66,4 +73,29 @@ function mergeMediaQueries(tokens) {
   return reduced;
 }
 
+function allSameRulePropertiesCanBeReordered(movedProperties, traversedProperties) {
+  var movedProperty;
+  var movedRule;
+  var traversedProperty;
+  var traversedRule;
+  var i, l;
+  var j, m;
+
+  for (i = 0, l = movedProperties.length; i < l; i++) {
+    movedProperty = movedProperties[i];
+    movedRule = movedProperty[5];
+
+    for (j = 0, m = traversedProperties.length; j < m; j++) {
+      traversedProperty = traversedProperties[i];
+      traversedRule = traversedProperty[5];
+
+      if (rulesOverlap(movedRule, traversedRule, true) && !canReorderSingle(movedProperty, traversedProperty)) {
+        return false;
+      }
+    }
+  }
+
+  return true;
+}
+
 module.exports = mergeMediaQueries;
diff --git a/lib/optimizer/rules-overlap.js b/lib/optimizer/rules-overlap.js
new file mode 100644 (file)
index 0000000..811a517
--- /dev/null
@@ -0,0 +1,32 @@
+var MODIFIER_PATTERN = /\-\-.+$/;
+
+function rulesOverlap(rule1, rule2, bemMode) {
+  var scope1;
+  var scope2;
+  var i, l;
+  var j, m;
+
+  for (i = 0, l = rule1.length; i < l; i++) {
+    scope1 = rule1[i][1];
+
+    for (j = 0, m = rule2.length; j < m; j++) {
+      scope2 = rule2[j][1];
+
+      if (scope1 == scope2) {
+        return true;
+      }
+
+      if (bemMode && withoutModifiers(scope1) == withoutModifiers(scope2)) {
+        return true;
+      }
+    }
+  }
+
+  return false;
+}
+
+function withoutModifiers(scope) {
+  return scope.replace(MODIFIER_PATTERN, '');
+}
+
+module.exports = rulesOverlap;
index 98e4676..f557b66 100644 (file)
@@ -90,6 +90,34 @@ vows.describe('merge media queries')
       ]
     })
   )
+  .addBatch(
+    optimizerContext('semantic merging mode', {
+      'moves over an otherwise blocking property': [
+        '@media (max-width:1px){.a{margin:1px}}.b{margin:2px}@media (max-width:1px){.c{margin:3px}}',
+        '.b{margin:2px}@media (max-width:1px){.a{margin:1px}.c{margin:3px}}'
+      ],
+      'moves over an otherwise blocking longhand property': [
+        '@media (max-width:1px){.a{margin:1px}}.a{margin-bottom:2px}@media (max-width:1px){.a{margin:3px}}',
+        '@media (max-width:1px){.a{margin:1px}}.a{margin-bottom:2px}@media (max-width:1px){.a{margin:3px}}'
+      ],
+      'does not move if separating selector redefines a property': [
+        '@media (max-width:1px){.a{margin:1px}}.a{margin:2px}@media (max-width:1px){.a{margin:3px}}',
+        '@media (max-width:1px){.a{margin:1px}}.a{margin:2px}@media (max-width:1px){.a{margin:3px}}'
+      ],
+      'does not move over blocking BEM block rules': [
+        '@media (max-width:1px){.block{margin:1px}}.block--modifier1{margin:2px}@media (max-width:1px){.block--modifier2{margin:3px}}',
+        '@media (max-width:1px){.block{margin:1px}}.block--modifier1{margin:2px}@media (max-width:1px){.block--modifier2{margin:3px}}'
+      ],
+      'does not move over blocking BEM element rules': [
+        '@media (max-width:1px){.block__element{margin:1px}}.block__element--modifier1{margin:2px}@media (max-width:1px){.block__element--modifier2{margin:3px}}',
+        '@media (max-width:1px){.block__element{margin:1px}}.block__element--modifier1{margin:2px}@media (max-width:1px){.block__element--modifier2{margin:3px}}'
+      ],
+      'moves over non-blocking BEM rules': [
+        '@media (max-width:1px){.block{margin:1px}}.block__element{margin:2px}@media (max-width:1px){.block--modifier{margin:3px}}',
+        '.block__element{margin:2px}@media (max-width:1px){.block{margin:1px}.block--modifier{margin:3px}}'
+      ]
+    }, { semanticMerging: true })
+  )
   .addBatch(
     optimizerContext('advanced off', {
       'keeps content same': [
diff --git a/test/optimizer/rules-overlap-test.js b/test/optimizer/rules-overlap-test.js
new file mode 100644 (file)
index 0000000..50b1be3
--- /dev/null
@@ -0,0 +1,92 @@
+var assert = require('assert');
+
+var vows = require('vows');
+
+var rulesOverlap = require('../../lib/optimizer/rules-overlap');
+
+vows.describe(rulesOverlap)
+  .addBatch({
+    'single non-overlapping scopes': {
+      'topic': function () {
+        return rulesOverlap(
+          [['rule-scope', '.one']],
+          [['rule-scope', '.two']]
+        );
+      },
+      'do not overlap': function (result) {
+        assert.isFalse(result);
+      }
+    },
+    'single overlapping scopes': {
+      'topic': function () {
+        return rulesOverlap(
+          [['rule-scope', '.one']],
+          [['rule-scope', '.one']]
+        );
+      },
+      'do overlap': function (result) {
+        assert.isTrue(result);
+      }
+    },
+    'multiple non-overlapping scopes': {
+      'topic': function () {
+        return rulesOverlap(
+          [['rule-scope', '.one'], ['rule-scope', '.two .three']],
+          [['rule-scope', '.two'], ['rule-scope', '.four']]
+        );
+      },
+      'do not overlap': function (result) {
+        assert.isFalse(result);
+      }
+    },
+    'multiple overlapping scopes': {
+      'topic': function () {
+        return rulesOverlap(
+          [['rule-scope', '.one'], ['rule-scope', '.four']],
+          [['rule-scope', '.one'], ['rule-scope', '.four']]
+        );
+      },
+      'do overlap': function (result) {
+        assert.isTrue(result);
+      }
+    }
+  })
+  .addBatch({
+    'single non-overlapping BEM scopes in BEM mode': {
+      'topic': function () {
+        return rulesOverlap(
+          [['rule-scope', '.one']],
+          [['rule-scope', '.two--modifier']],
+          true
+        );
+      },
+      'do overlap': function (result) {
+        assert.isFalse(result);
+      }
+    },
+    'single overlapping BEM scopes in BEM mode': {
+      'topic': function () {
+        return rulesOverlap(
+          [['rule-scope', '.one']],
+          [['rule-scope', '.one--modifier']],
+          true
+        );
+      },
+      'do overlap': function (result) {
+        assert.isTrue(result);
+      }
+    },
+    'single overlapping BEM scopes with modifiers in BEM mode': {
+      'topic': function () {
+        return rulesOverlap(
+          [['rule-scope', '.one--modifier1']],
+          [['rule-scope', '.one--modifier2']],
+          true
+        );
+      },
+      'do overlap': function (result) {
+        assert.isTrue(result);
+      }
+    },
+  })
+  .export(module);