Fixes #425 - enables natural method of sorting selectors.
authorJakub Pawlowicz <contact@jakubpawlowicz.com>
Tue, 10 Jan 2017 17:04:07 +0000 (18:04 +0100)
committerJakub Pawlowicz <contact@jakubpawlowicz.com>
Wed, 11 Jan 2017 12:18:29 +0000 (13:18 +0100)
Still defaults to standard sorting but natural becomes
a new option. Controlled via `selectorsSortingMethod`
option in level 1 optimizations.

Why:

* Natural way could be a more compression-efficient way;
* it's easier to read for a human.

README.md
bin/cleancss
lib/optimizer/level-1/optimize.js
lib/optimizer/level-1/sort-selectors.js [new file with mode: 0644]
lib/optimizer/level-1/tidy-rules.js
lib/optimizer/level-2/merge-adjacent.js
lib/optimizer/level-2/merge-non-adjacent-by-body.js
lib/options/optimization-level.js
test/optimizer/level-1/optimize-test.js
test/options/optimization-level-test.js

index 9b339e6..efe5177 100644 (file)
--- a/README.md
+++ b/README.md
@@ -136,6 +136,7 @@ cleancss -O1 all:off;specialComments:1 one.css
 # `replaceTimeUnits` controls replacing time units with shorter values; defaults to `on
 # `replaceZeroUnits` controls replacing zero values with units; defaults to `on`
 # `roundingPrecision` rounds pixel values to `N` decimal places; `off` disables rounding; defaults to `off`
+# `selectorsSortingMethod` denotes selector sorting method; can be `natural` or `standard`; defaults to `standard`
 # `specialComments` denotes a number of /*! ... */ comments preserved; defaults to `all`
 # `tidyAtRules` controls at-rules (e.g. `@charset`, `@import`) optimizing; defaults to `on`
 # `tidyBlockScopes` controls block scopes (e.g. `@media`) optimizing; defaults to `on`
@@ -222,6 +223,7 @@ new CleanCSS({
       replaceTimeUnits: true, // controls replacing time units with shorter values; defaults to `true`
       replaceZeroUnits: true, // controls replacing zero values with units; defaults to `true`
       roundingPrecision: false, // rounds pixel values to `N` decimal places; `false` disables rounding; defaults to `false`
+      selectorsSortingMethod: 'standard', // denotes selector sorting method; can be `natural` or `standard`; defaults to `standard`
       specialComments: 'all', // denotes a number of /*! ... */ comments preserved; defaults to `all`
       tidyAtRules: true, // controls at-rules (e.g. `@charset`, `@import`) optimizing; defaults to `true`
       tidyBlockScopes: true, // controls block scopes (e.g. `@media`) optimizing; defaults to `true`
index 06cd8c2..30143d4 100755 (executable)
@@ -55,6 +55,7 @@ commands.on('--help', function () {
   console.log('    %> # `replaceTimeUnits` controls replacing time units with shorter values; defaults to `on');
   console.log('    %> # `replaceZeroUnits` controls replacing zero values with units; defaults to `on`');
   console.log('    %> # `roundingPrecision` rounds pixel values to `N` decimal places; `off` disables rounding; defaults to `off`');
+  console.log('    %> # `selectorsSortingMethod` denotes selector sorting method; can be `natural` or `standard`; defaults to `standard`');
   console.log('    %> # `specialComments` denotes a number of /*! ... */ comments preserved; defaults to `all`');
   console.log('    %> # `tidyAtRules` controls at-rules (e.g. `@charset`, `@import`) optimizing; defaults to `on`');
   console.log('    %> # `tidyBlockScopes` controls block scopes (e.g. `@media`) optimizing; defaults to `on`');
index ac9b075..78ab4a3 100644 (file)
@@ -1,6 +1,7 @@
 var shortenHex = require('./shorten-hex');
 var shortenHsl = require('./shorten-hsl');
 var shortenRgb = require('./shorten-rgb');
+var sortSelectors = require('./sort-selectors');
 var tidyRules = require('./tidy-rules');
 var tidyBlock = require('./tidy-block');
 var tidyAtRule = require('./tidy-at-rule');
@@ -664,6 +665,7 @@ function level1Optimize(tokens, context) {
         break;
       case Token.RULE:
         token[1] = levelOptions.tidySelectors ? tidyRules(token[1], !ie7Hack, adjacentSpace, beautify, context.warnings) : token[1];
+        token[1] = token[1].length > 1 ? sortSelectors(token[1], levelOptions.selectorsSortingMethod) : token[1];
         optimizeBody(token[2], context);
         afterRules = true;
         break;
diff --git a/lib/optimizer/level-1/sort-selectors.js b/lib/optimizer/level-1/sort-selectors.js
new file mode 100644 (file)
index 0000000..2f179e6
--- /dev/null
@@ -0,0 +1,25 @@
+var naturalCompare = require('../../utils/natural-compare');
+
+function naturalSorter(scope1, scope2) {
+  return naturalCompare(scope1[1], scope2[1]);
+}
+
+function standardSorter(scope1, scope2) {
+  return scope1[1] > scope2[1] ? 1 : -1;
+}
+
+function sortSelectors(selectors, method) {
+  var sorter;
+
+  switch (method) {
+    case 'natural':
+      sorter = naturalSorter;
+      break;
+    case 'standard':
+      sorter = standardSorter;
+  }
+
+  return selectors.sort(sorter);
+}
+
+module.exports = sortSelectors;
index 84e9dc3..cb19215 100644 (file)
@@ -135,10 +135,6 @@ function removeQuotes(value) {
     .replace(/="([a-zA-Z][a-zA-Z\d\-_]+)"/g, '=$1');
 }
 
-function ruleSorter(s1, s2) {
-  return s1[1] > s2[1] ? 1 : -1;
-}
-
 function tidyRules(rules, removeUnsupported, adjacentSpace, beautify, warnings) {
   var list = [];
   var repeated = [];
@@ -194,7 +190,7 @@ function tidyRules(rules, removeUnsupported, adjacentSpace, beautify, warnings)
     list = [];
   }
 
-  return list.sort(ruleSorter);
+  return list;
 }
 
 module.exports = tidyRules;
index 4674328..8a93304 100644 (file)
@@ -2,8 +2,11 @@ var isMergeable = require('./is-mergeable');
 
 var compactorOptimize = require('./compacting/optimize');
 
+var sortSelectors = require('../level-1/sort-selectors');
 var tidyRules = require('../level-1/tidy-rules');
 
+var OptimizationLevel = require('../../options/optimization-level').OptimizationLevel;
+
 var serializeBody = require('../../writer/one-time').body;
 var serializeRules = require('../../writer/one-time').rules;
 
@@ -13,6 +16,7 @@ function mergeAdjacent(tokens, context) {
   var lastToken = [null, [], []];
   var options = context.options;
   var adjacentSpace = options.compatibility.selectors.adjacentSpace;
+  var selectorsSortingMethod = options.level[OptimizationLevel.One].selectorsSortingMethod;
   var mergeablePseudoClasses = options.compatibility.selectors.mergeablePseudoClasses;
   var mergeablePseudoElements = options.compatibility.selectors.mergeablePseudoElements;
 
@@ -33,6 +37,7 @@ function mergeAdjacent(tokens, context) {
         isMergeable(serializeRules(token[1]), mergeablePseudoClasses, mergeablePseudoElements) &&
         isMergeable(serializeRules(lastToken[1]), mergeablePseudoClasses, mergeablePseudoElements)) {
       lastToken[1] = tidyRules(lastToken[1].concat(token[1]), false, adjacentSpace, false, context.warnings);
+      lastToken[1] = lastToken.length > 1 ? sortSelectors(lastToken[1], selectorsSortingMethod) : lastToken[1];
       token[2] = [];
     } else {
       lastToken = token;
index 5046b04..bdcf53e 100644 (file)
@@ -1,5 +1,6 @@
 var isMergeable = require('./is-mergeable');
 
+var sortSelectors = require('../level-1/sort-selectors');
 var tidyRules = require('../level-1/tidy-rules');
 
 var OptimizationLevel = require('../../options/optimization-level').OptimizationLevel;
@@ -38,6 +39,7 @@ function mergeNonAdjacentByBody(tokens, context) {
   var options = context.options;
   var mergeSemantically = options.level[OptimizationLevel.Two].mergeSemantically;
   var adjacentSpace = options.compatibility.selectors.adjacentSpace;
+  var selectorsSortingMethod = options.level[OptimizationLevel.One].selectorsSortingMethod;
   var mergeablePseudoClasses = options.compatibility.selectors.mergeablePseudoClasses;
   var mergeablePseudoElements = options.compatibility.selectors.mergeablePseudoElements;
   var candidates = {};
@@ -58,9 +60,13 @@ function mergeNonAdjacentByBody(tokens, context) {
     if (oldToken &&
         isMergeable(serializeRules(token[1]), mergeablePseudoClasses, mergeablePseudoElements) &&
         isMergeable(serializeRules(oldToken[1]), mergeablePseudoClasses, mergeablePseudoElements)) {
-      token[1] = token[2].length > 0 ?
-        tidyRules(oldToken[1].concat(token[1]), false, adjacentSpace, false, context.warnings) :
-        oldToken[1].concat(token[1]);
+
+      if (token[2].length > 0) {
+        token[1] = tidyRules(oldToken[1].concat(token[1]), false, adjacentSpace, false, context.warnings);
+        token[1] = token[1].length > 1 ? sortSelectors(token[1], selectorsSortingMethod) : token[1];
+      } else {
+        token[1] = oldToken[1].concat(token[1]);
+      }
 
       oldToken[2] = [];
       candidates[candidateBody] = null;
index 51d2d2e..c0ce816 100644 (file)
@@ -26,6 +26,7 @@ DEFAULTS[OptimizationLevel.One] = {
   replaceTimeUnits: true,
   replaceZeroUnits: true,
   roundingPrecision: roundingPrecisionFrom(undefined),
+  selectorsSortingMethod: 'standard',
   specialComments: 'all',
   tidyAtRules: true,
   tidyBlockScopes: true,
index ac7d6ee..fdf70b1 100644 (file)
@@ -131,6 +131,34 @@ vows.describe('level 1 optimizations')
       ]
     }, { level: 1 })
   )
+  .addBatch(
+    optimizerContext('selectors - sorting when tidySelectors is off', {
+      'no numbers': [
+        '.block,.another-block,.one-more-block{color:red}',
+        '.another-block,.block,.one-more-block{color:red}'
+      ]
+    }, { level: { 1: { tidySelectors: false } } })
+  )
+  .addBatch(
+    optimizerContext('selectors - natural order', {
+      'no numbers': [
+        '.block,.another-block,.one-more-block{color:red}',
+        '.another-block,.block,.one-more-block{color:red}'
+      ],
+      'some numbers': [
+        '.block-3,.block-11,.block{color:red}',
+        '.block,.block-3,.block-11{color:red}'
+      ],
+      'all numbers': [
+        '.block-3,.block-11,.block-1{color:red}',
+        '.block-1,.block-3,.block-11{color:red}'
+      ],
+      'complex numbers': [
+        '.block-1__element-11,.block-1__element-2,.block-12__element-1,.block-3__element-1{color:red}',
+        '.block-1__element-2,.block-1__element-11,.block-3__element-1,.block-12__element-1{color:red}'
+      ],
+    }, { level: { 1: { selectorsSortingMethod: 'natural' } } })
+  )
   .addBatch(
     optimizerContext('selectors - ie8', {
       '+html': [
index 2cb1c58..c2369f6 100644 (file)
@@ -34,6 +34,7 @@ vows.describe(optimizationLevelFrom)
           replaceTimeUnits: true,
           replaceZeroUnits: true,
           roundingPrecision: roundingPrecisionFrom(undefined),
+          selectorsSortingMethod: 'standard',
           specialComments: 'all',
           tidyAtRules: true,
           tidyBlockScopes: true,
@@ -79,6 +80,7 @@ vows.describe(optimizationLevelFrom)
           replaceTimeUnits: true,
           replaceZeroUnits: true,
           roundingPrecision: roundingPrecisionFrom(undefined),
+          selectorsSortingMethod: 'standard',
           specialComments: 'all',
           tidyAtRules: true,
           tidyBlockScopes: true,
@@ -113,6 +115,7 @@ vows.describe(optimizationLevelFrom)
           replaceTimeUnits: true,
           replaceZeroUnits: true,
           roundingPrecision: roundingPrecisionFrom(undefined),
+          selectorsSortingMethod: 'standard',
           specialComments: 'all',
           tidyAtRules: true,
           tidyBlockScopes: true,
@@ -172,6 +175,7 @@ vows.describe(optimizationLevelFrom)
           replaceTimeUnits: true,
           replaceZeroUnits: true,
           roundingPrecision: roundingPrecisionFrom(undefined),
+          selectorsSortingMethod: 'standard',
           specialComments: 0,
           tidyAtRules: true,
           tidyBlockScopes: true,
@@ -220,6 +224,7 @@ vows.describe(optimizationLevelFrom)
           replaceTimeUnits: false,
           replaceZeroUnits: false,
           roundingPrecision: roundingPrecisionFrom(undefined),
+          selectorsSortingMethod: 'standard',
           specialComments: 'all',
           tidyAtRules: false,
           tidyBlockScopes: false,
@@ -254,6 +259,7 @@ vows.describe(optimizationLevelFrom)
           replaceTimeUnits: false,
           replaceZeroUnits: false,
           roundingPrecision: roundingPrecisionFrom(undefined),
+          selectorsSortingMethod: 'standard',
           specialComments: 'all',
           tidyAtRules: false,
           tidyBlockScopes: false,
@@ -288,6 +294,7 @@ vows.describe(optimizationLevelFrom)
           replaceTimeUnits: true,
           replaceZeroUnits: true,
           roundingPrecision: roundingPrecisionFrom(undefined),
+          selectorsSortingMethod: 'standard',
           specialComments: 0,
           tidyAtRules: true,
           tidyBlockScopes: true,
@@ -336,6 +343,7 @@ vows.describe(optimizationLevelFrom)
           replaceTimeUnits: true,
           replaceZeroUnits: true,
           roundingPrecision: roundingPrecisionFrom(undefined),
+          selectorsSortingMethod: 'standard',
           specialComments: 0,
           tidyAtRules: true,
           tidyBlockScopes: true,
@@ -384,6 +392,7 @@ vows.describe(optimizationLevelFrom)
           replaceTimeUnits: true,
           replaceZeroUnits: true,
           roundingPrecision: roundingPrecisionFrom(3),
+          selectorsSortingMethod: 'standard',
           specialComments: 0,
           tidyAtRules: true,
           tidyBlockScopes: true,
@@ -418,6 +427,7 @@ vows.describe(optimizationLevelFrom)
           replaceTimeUnits: true,
           replaceZeroUnits: true,
           roundingPrecision: roundingPrecisionFrom(undefined),
+          selectorsSortingMethod: 'standard',
           specialComments: 'all',
           tidyAtRules: true,
           tidyBlockScopes: true,
@@ -466,6 +476,7 @@ vows.describe(optimizationLevelFrom)
           replaceTimeUnits: true,
           replaceZeroUnits: true,
           roundingPrecision: roundingPrecisionFrom(undefined),
+          selectorsSortingMethod: 'standard',
           specialComments: 'all',
           tidyAtRules: true,
           tidyBlockScopes: true,
@@ -514,6 +525,7 @@ vows.describe(optimizationLevelFrom)
           replaceTimeUnits: true,
           replaceZeroUnits: true,
           roundingPrecision: roundingPrecisionFrom(undefined),
+          selectorsSortingMethod: 'standard',
           specialComments: 'all',
           tidyAtRules: true,
           tidyBlockScopes: true,
@@ -562,6 +574,7 @@ vows.describe(optimizationLevelFrom)
           replaceTimeUnits: true,
           replaceZeroUnits: true,
           roundingPrecision: roundingPrecisionFrom(undefined),
+          selectorsSortingMethod: 'standard',
           specialComments: 'all',
           tidyAtRules: true,
           tidyBlockScopes: true,
@@ -610,6 +623,7 @@ vows.describe(optimizationLevelFrom)
           replaceTimeUnits: true,
           replaceZeroUnits: true,
           roundingPrecision: roundingPrecisionFrom(undefined),
+          selectorsSortingMethod: 'standard',
           specialComments: 'all',
           tidyAtRules: true,
           tidyBlockScopes: true,
@@ -661,6 +675,7 @@ vows.describe(optimizationLevelFrom)
             'vw': 4,
             '%': 4
           },
+          selectorsSortingMethod: 'standard',
           specialComments: 'all',
           tidyAtRules: true,
           tidyBlockScopes: true,
@@ -712,6 +727,7 @@ vows.describe(optimizationLevelFrom)
             'vw': 5,
             '%': 1
           },
+          selectorsSortingMethod: 'standard',
           specialComments: 'all',
           tidyAtRules: true,
           tidyBlockScopes: true,