From 9bfee7c7b34665e950c78f044df5e6b688ac5512 Mon Sep 17 00:00:00 2001 From: Jakub Pawlowicz Date: Tue, 3 Jan 2017 17:37:56 +0100 Subject: [PATCH] Fixes #796 - enables semantic merging for `@media` blocks. * It's off by default and can be enabled via `semanticMerging` flag; * Handles all cases and BEM classes. --- lib/optimizer/merge-media-queries.js | 34 +++++++- lib/optimizer/rules-overlap.js | 32 ++++++++ test/optimizer/merge-media-queries-test.js | 28 +++++++ test/optimizer/rules-overlap-test.js | 92 ++++++++++++++++++++++ 4 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 lib/optimizer/rules-overlap.js create mode 100644 test/optimizer/rules-overlap-test.js diff --git a/lib/optimizer/merge-media-queries.js b/lib/optimizer/merge-media-queries.js index 711dfa2d..a8adbad7 100644 --- a/lib/optimizer/merge-media-queries.js +++ b/lib/optimizer/merge-media-queries.js @@ -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 index 00000000..811a517b --- /dev/null +++ b/lib/optimizer/rules-overlap.js @@ -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; diff --git a/test/optimizer/merge-media-queries-test.js b/test/optimizer/merge-media-queries-test.js index 98e4676d..f557b664 100644 --- a/test/optimizer/merge-media-queries-test.js +++ b/test/optimizer/merge-media-queries-test.js @@ -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 index 00000000..50b1be31 --- /dev/null +++ b/test/optimizer/rules-overlap-test.js @@ -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); -- 2.34.1