From: Jakub Pawlowicz Date: Tue, 10 Jan 2017 17:04:07 +0000 (+0100) Subject: Fixes #425 - enables natural method of sorting selectors. X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=d0320e81b216c083af67e85eb51aec1d42c67e08;p=clean-css.git Fixes #425 - enables natural method of sorting selectors. 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. --- diff --git a/README.md b/README.md index 9b339e6a..efe5177a 100644 --- 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` diff --git a/bin/cleancss b/bin/cleancss index 06cd8c25..30143d48 100755 --- a/bin/cleancss +++ b/bin/cleancss @@ -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`'); diff --git a/lib/optimizer/level-1/optimize.js b/lib/optimizer/level-1/optimize.js index ac9b0752..78ab4a39 100644 --- a/lib/optimizer/level-1/optimize.js +++ b/lib/optimizer/level-1/optimize.js @@ -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 index 00000000..2f179e6e --- /dev/null +++ b/lib/optimizer/level-1/sort-selectors.js @@ -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; diff --git a/lib/optimizer/level-1/tidy-rules.js b/lib/optimizer/level-1/tidy-rules.js index 84e9dc33..cb192151 100644 --- a/lib/optimizer/level-1/tidy-rules.js +++ b/lib/optimizer/level-1/tidy-rules.js @@ -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; diff --git a/lib/optimizer/level-2/merge-adjacent.js b/lib/optimizer/level-2/merge-adjacent.js index 4674328e..8a93304d 100644 --- a/lib/optimizer/level-2/merge-adjacent.js +++ b/lib/optimizer/level-2/merge-adjacent.js @@ -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; diff --git a/lib/optimizer/level-2/merge-non-adjacent-by-body.js b/lib/optimizer/level-2/merge-non-adjacent-by-body.js index 5046b046..bdcf53ec 100644 --- a/lib/optimizer/level-2/merge-non-adjacent-by-body.js +++ b/lib/optimizer/level-2/merge-non-adjacent-by-body.js @@ -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; diff --git a/lib/options/optimization-level.js b/lib/options/optimization-level.js index 51d2d2e8..c0ce816c 100644 --- a/lib/options/optimization-level.js +++ b/lib/options/optimization-level.js @@ -26,6 +26,7 @@ DEFAULTS[OptimizationLevel.One] = { replaceTimeUnits: true, replaceZeroUnits: true, roundingPrecision: roundingPrecisionFrom(undefined), + selectorsSortingMethod: 'standard', specialComments: 'all', tidyAtRules: true, tidyBlockScopes: true, diff --git a/test/optimizer/level-1/optimize-test.js b/test/optimizer/level-1/optimize-test.js index ac7d6ee4..fdf70b14 100644 --- a/test/optimizer/level-1/optimize-test.js +++ b/test/optimizer/level-1/optimize-test.js @@ -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': [ diff --git a/test/options/optimization-level-test.js b/test/options/optimization-level-test.js index 2cb1c58d..c2369f6b 100644 --- a/test/options/optimization-level-test.js +++ b/test/options/optimization-level-test.js @@ -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,