From: Jakub Pawlowicz Date: Mon, 29 Sep 2014 12:01:20 +0000 (+0100) Subject: Improves selector optimizer. X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=6581b5a98ed67d290f0e2ebd56559d830dcda793;p=clean-css.git Improves selector optimizer. * Turns to prototypal OO. * Adds specs. * Extracts Simple & Advanced optimizers into separate modules. * Adds nasty workarounds as property optimizer is incompatible with new tokenizer output. --- diff --git a/lib/selectors/optimizer.js b/lib/selectors/optimizer.js index c5e6b6e0..bfc31c8b 100644 --- a/lib/selectors/optimizer.js +++ b/lib/selectors/optimizer.js @@ -1,341 +1,39 @@ var Tokenizer = require('./tokenizer'); -var PropertyOptimizer = require('../properties/optimizer'); - -module.exports = function Optimizer(options, context) { - var specialSelectors = { - '*': /\-(moz|ms|o|webkit)\-/, - 'ie8': /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not)/, - 'ie7': /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:focus|:before|:after|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not)/ - }; - - var minificationsMade = []; - - var propertyOptimizer = new PropertyOptimizer(options.compatibility, options.aggressiveMerging, context); - - var cleanUpSelector = function(selectors) { - if (selectors.indexOf(',') == -1) - return selectors; - - var plain = []; - var cursor = 0; - var lastComma = 0; - var noBrackets = selectors.indexOf('(') == -1; - var withinBrackets = function(idx) { - if (noBrackets) - return false; - - var previousOpening = selectors.lastIndexOf('(', idx); - var previousClosing = selectors.lastIndexOf(')', idx); - - if (previousOpening == -1) - return false; - if (previousClosing > 0 && previousClosing < idx) - return false; - - return true; - }; - - while (true) { - var nextComma = selectors.indexOf(',', cursor + 1); - var selector; - - if (nextComma === -1) { - nextComma = selectors.length; - } else if (withinBrackets(nextComma)) { - cursor = nextComma + 1; - continue; - } - selector = selectors.substring(lastComma, nextComma); - lastComma = cursor = nextComma + 1; - - if (plain.indexOf(selector) == -1) - plain.push(selector); - - if (nextComma === selectors.length) - break; - } - - return plain.sort().join(','); - }; - - var isSpecial = function(selector) { - return specialSelectors[options.compatibility || '*'].test(selector); - }; - - var removeDuplicates = function(tokens) { - var matched = {}; - var forRemoval = []; - - for (var i = 0, l = tokens.length; i < l; i++) { - var token = tokens[i]; - if (typeof token == 'string' || token.block) - continue; - - var id = token.body + '@' + token.selector; - var alreadyMatched = matched[id]; - - if (alreadyMatched) { - forRemoval.push(alreadyMatched[0]); - alreadyMatched.unshift(i); - } else { - matched[id] = [i]; - } - } - - forRemoval = forRemoval.sort(function(a, b) { - return a > b ? 1 : -1; - }); - - for (var j = 0, n = forRemoval.length; j < n; j++) { - tokens.splice(forRemoval[j] - j, 1); - } - - minificationsMade.unshift(forRemoval.length > 0); - }; - - var mergeAdjacent = function(tokens) { - var forRemoval = []; - var lastToken = { selector: null, body: null }; - - for (var i = 0, l = tokens.length; i < l; i++) { - var token = tokens[i]; - - if (typeof token == 'string' || token.block) - continue; - - if (token.selector == lastToken.selector) { - var joinAt = [lastToken.body.split(';').length]; - lastToken.body = propertyOptimizer.process(lastToken.body + ';' + token.body, joinAt, false, token.selector); - forRemoval.push(i); - } else if (token.body == lastToken.body && !isSpecial(token.selector) && !isSpecial(lastToken.selector)) { - lastToken.selector = cleanUpSelector(lastToken.selector + ',' + token.selector); - forRemoval.push(i); - } else { - lastToken = token; - } - } - - for (var j = 0, m = forRemoval.length; j < m; j++) { - tokens.splice(forRemoval[j] - j, 1); - } - - minificationsMade.unshift(forRemoval.length > 0); - }; - - var reduceNonAdjacent = function(tokens) { - var candidates = {}; - var moreThanOnce = []; - - for (var i = tokens.length - 1; i >= 0; i--) { - var token = tokens[i]; - - if (typeof token == 'string' || token.block) - continue; - - var complexSelector = token.selector; - var selectors = complexSelector.indexOf(',') > -1 && !isSpecial(complexSelector) ? - complexSelector.split(',').concat(complexSelector) : // simplification, as :not() can have commas too - [complexSelector]; - - for (var j = 0, m = selectors.length; j < m; j++) { - var selector = selectors[j]; - - if (!candidates[selector]) - candidates[selector] = []; - else - moreThanOnce.push(selector); - - candidates[selector].push({ - where: i, - partial: selector != complexSelector - }); - } - } - - var reducedInSimple = _reduceSimpleNonAdjacentCases(tokens, moreThanOnce, candidates); - var reducedInComplex = _reduceComplexNonAdjacentCases(tokens, candidates); - - minificationsMade.unshift(reducedInSimple || reducedInComplex); - }; - - var _reduceSimpleNonAdjacentCases = function(tokens, matches, positions) { - var reduced = false; - - for (var i = 0, l = matches.length; i < l; i++) { - var selector = matches[i]; - var data = positions[selector]; - - if (data.length < 2) - continue; - - /* jshint loopfunc: true */ - _reduceSelector(tokens, selector, data, { - filterOut: function(idx, bodies) { - return data[idx].partial && bodies.length === 0; - }, - callback: function(token, newBody, processedCount, tokenIdx) { - if (!data[processedCount - tokenIdx - 1].partial) { - token.body = newBody.join(';'); - reduced = true; - } - } - }); - } - - return reduced; - }; - - var _reduceComplexNonAdjacentCases = function(tokens, positions) { - var reduced = false; - - allSelectors: - for (var complexSelector in positions) { - if (complexSelector.indexOf(',') == -1) // simplification, as :not() can have commas too - continue; - - var intoPosition = positions[complexSelector].pop().where; - var intoToken = tokens[intoPosition]; - - var selectors = isSpecial(complexSelector) ? - [complexSelector] : - complexSelector.split(','); - var reducedBodies = []; - - for (var j = 0, m = selectors.length; j < m; j++) { - var selector = selectors[j]; - var data = positions[selector]; - - if (data.length < 2) - continue allSelectors; - - /* jshint loopfunc: true */ - _reduceSelector(tokens, selector, data, { - filterOut: function(idx) { - return data[idx].where < intoPosition; - }, - callback: function(token, newBody, processedCount, tokenIdx) { - if (tokenIdx === 0) - reducedBodies.push(newBody.join(';')); - } - }); - - if (reducedBodies[reducedBodies.length - 1] != reducedBodies[0]) - continue allSelectors; - } - - intoToken.body = reducedBodies[0]; - reduced = true; - } - - return reduced; - }; - - var _reduceSelector = function(tokens, selector, data, options) { - var bodies = []; - var joinsAt = []; - var splitBodies = []; - var processedTokens = []; - - for (var j = data.length - 1, m = 0; j >= 0; j--) { - if (options.filterOut(j, bodies)) - continue; - - var where = data[j].where; - var token = tokens[where]; - var body = token.body; - bodies.push(body); - splitBodies.push(body.split(';')); - processedTokens.push(where); - } - - for (j = 0, m = bodies.length; j < m; j++) { - if (bodies[j].length > 0) - joinsAt.push((joinsAt[j - 1] || 0) + splitBodies[j].length); - } - - var optimizedBody = propertyOptimizer.process(bodies.join(';'), joinsAt, true, selector); - var optimizedProperties = optimizedBody.split(';'); - - var processedCount = processedTokens.length; - var propertyIdx = optimizedProperties.length - 1; - var tokenIdx = processedCount - 1; - - while (tokenIdx >= 0) { - if ((tokenIdx === 0 || splitBodies[tokenIdx].indexOf(optimizedProperties[propertyIdx]) > -1) && propertyIdx > -1) { - propertyIdx--; - continue; - } - - var newBody = optimizedProperties.splice(propertyIdx + 1); - options.callback(tokens[processedTokens[tokenIdx]], newBody, processedCount, tokenIdx); - - tokenIdx--; - } - }; - - var optimize = function(tokens) { - var noChanges = function() { - return minificationsMade.length > 4 && - minificationsMade[0] === false && - minificationsMade[1] === false; - }; - - tokens = Array.isArray(tokens) ? tokens : [tokens]; - for (var i = 0, l = tokens.length; i < l; i++) { - var token = tokens[i]; - - if (token.selector) { - token.selector = cleanUpSelector(token.selector); - token.body = propertyOptimizer.process(token.body, false, false, token.selector); - } else if (token.block) { - optimize(token.body); - } - } - - // Run until 2 last operations do not yield any changes - minificationsMade = []; - while (true) { - if (noChanges()) - break; - removeDuplicates(tokens); - - if (noChanges()) - break; - mergeAdjacent(tokens); - - if (noChanges()) - break; - reduceNonAdjacent(tokens); - } - }; - - var rebuild = function(tokens) { - var rebuilt = []; - - tokens = Array.isArray(tokens) ? tokens : [tokens]; - for (var i = 0, l = tokens.length; i < l; i++) { - var token = tokens[i]; - - if (typeof token == 'string') { - rebuilt.push(token); - continue; - } - - var name = token.block || token.selector; - var body = token.block ? rebuild(token.body) : token.body; - - if (body.length > 0) - rebuilt.push(name + '{' + body + '}'); - } - - return rebuilt.join(options.keepBreaks ? options.lineBreak : ''); - }; - - return { - process: function(data) { - var tokenized = new Tokenizer(context).toTokens(data); - // optimize(tokenized); - return rebuild(tokenized); - } - }; +var SimpleOptimizer = require('./optimizers/simple'); +var AdvancedOptimizer = require('./optimizers/advanced'); + +var lineBreak = require('os').EOL; + +function SelectorsOptimizer(options, context) { + this.options = options || {}; + this.context = context || {}; +} + +function rebuild(tokens, keepBreaks) { + return tokens + .map(function (token) { + if (typeof token === 'string') + return token; + // TODO: broken due to joining/splitting + if (token.body.length === 0 || (token.body.length == 1 && token.body[0] === '')) + return ''; + + return token.block ? + token.block + '{' + rebuild(token.body, keepBreaks) + '}' : + token.selector.join(',') + '{' + token.body.join(';') + '}'; + }) + .join(keepBreaks ? lineBreak : '') + .trim(); +} + +SelectorsOptimizer.prototype.process = function (data) { + var tokens = new Tokenizer(this.context).toTokens(data); + + new SimpleOptimizer(this.options, this.context).optimize(tokens); + if (!this.options.noAdvanced) + new AdvancedOptimizer(this.options, this.context).optimize(tokens); + + return rebuild(tokens, this.options.keepBreaks); }; + +module.exports = SelectorsOptimizer; diff --git a/lib/selectors/optimizers/advanced.js b/lib/selectors/optimizers/advanced.js new file mode 100644 index 00000000..55267742 --- /dev/null +++ b/lib/selectors/optimizers/advanced.js @@ -0,0 +1,280 @@ +var PropertyOptimizer = require('../../properties/optimizer'); +var CleanUp = require('./clean-up'); + +var specialSelectors = { + '*': /\-(moz|ms|o|webkit)\-/, + 'ie8': /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not)/, + 'ie7': /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:focus|:before|:after|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not)/ +}; + +function AdvancedOptimizer(options, context) { + this.options = options; + this.minificationsMade = []; + this.propertyOptimizer = new PropertyOptimizer(this.options.compatibility, this.options.aggressiveMerging, context); +} + +AdvancedOptimizer.prototype.isSpecial = function (selector) { + return specialSelectors[this.options.compatibility || '*'].test(selector); +}; + +AdvancedOptimizer.prototype.removeDuplicates = function (tokens) { + var matched = {}; + var forRemoval = []; + + for (var i = 0, l = tokens.length; i < l; i++) { + var token = tokens[i]; + if (typeof token == 'string' || token.block) + continue; + + var id = token.body.join(';') + '@' + token.selector.join(','); + var alreadyMatched = matched[id]; + + if (alreadyMatched) { + forRemoval.push(alreadyMatched[0]); + alreadyMatched.unshift(i); + } else { + matched[id] = [i]; + } + } + + forRemoval = forRemoval.sort(function(a, b) { + return a > b ? 1 : -1; + }); + + for (var j = 0, n = forRemoval.length; j < n; j++) { + tokens.splice(forRemoval[j] - j, 1); + } + + this.minificationsMade.unshift(forRemoval.length > 0); +}; + +AdvancedOptimizer.prototype.mergeAdjacent = function (tokens) { + var forRemoval = []; + var lastToken = { selector: null, body: null }; + + for (var i = 0, l = tokens.length; i < l; i++) { + var token = tokens[i]; + + if (typeof token == 'string' || token.block) + continue; + + // TODO: broken due to joining/splitting + if (lastToken.selector && token.selector.join(',') == lastToken.selector.join(',')) { + var joinAt = [lastToken.body.length]; + lastToken.body = lastToken.body.concat(token.body); + // TODO: broken due to joining/splitting + lastToken.body = this.propertyOptimizer.process(lastToken.body.concat(token.body).join(';'), joinAt, false, token.selector.join(',')).split(';'); + forRemoval.push(i); + // TODO: broken due to joining/splitting + } else if (lastToken.body && token.body.join(';') == lastToken.body.join(';') && !this.isSpecial(token.selector.join(',')) && !this.isSpecial(lastToken.selector.join(','), this.options)) { + lastToken.selector = CleanUp.selectors(lastToken.selector.concat(token.selector)); + forRemoval.push(i); + } else { + lastToken = token; + } + } + + for (var j = 0, m = forRemoval.length; j < m; j++) { + tokens.splice(forRemoval[j] - j, 1); + } + + this.minificationsMade.unshift(forRemoval.length > 0); +}; + +AdvancedOptimizer.prototype.reduceNonAdjacent = function (tokens) { + var candidates = {}; + var moreThanOnce = []; + + for (var i = tokens.length - 1; i >= 0; i--) { + var token = tokens[i]; + + if (typeof token == 'string' || token.block) + continue; + + var complexSelector = token.selector; + var selectors = complexSelector.length > 1 && !this.isSpecial(complexSelector, this.options) ? + complexSelector : + [complexSelector]; + + for (var j = 0, m = selectors.length; j < m; j++) { + // TODO: broken due to joining/splitting + var selector = selectors[j]; + + if (!candidates[selector]) + candidates[selector] = []; + else + moreThanOnce.push(selector); + + // TODO: broken due to joining/splitting + candidates[selector].push({ + where: i, + partial: selector != complexSelector.join(',') + }); + } + } + + var reducedInSimple = this.reduceSimpleNonAdjacentCases(tokens, moreThanOnce, candidates); + var reducedInComplex = this.reduceComplexNonAdjacentCases(tokens, candidates); + + this.minificationsMade.unshift(reducedInSimple || reducedInComplex); +}; + +AdvancedOptimizer.prototype.reduceSimpleNonAdjacentCases = function (tokens, matches, positions) { + var reduced = false; + + for (var i = 0, l = matches.length; i < l; i++) { + var selector = matches[i]; + var data = positions[selector]; + + if (data.length < 2) + continue; + + /* jshint loopfunc: true */ + this.reduceSelector(tokens, selector, data, { + filterOut: function (idx, bodies) { + return data[idx].partial && bodies.length === 0; + }, + callback: function (token, newBody, processedCount, tokenIdx) { + if (!data[processedCount - tokenIdx - 1].partial) { + token.body = newBody; + reduced = true; + } + } + }); + } + + return reduced; +}; + +AdvancedOptimizer.prototype.reduceComplexNonAdjacentCases = function (tokens, positions) { + var reduced = false; + + allSelectors: + for (var complexSelector in positions) { + if (positions[complexSelector].length == 1) + continue; + + var into = positions[complexSelector]; + var intoPosition = into[into.length - 1].where; + var intoToken = tokens[intoPosition]; + + // TODO: broken due to joining/splitting + // var selectors = this.isSpecial(complexSelector) ? + // [complexSelector] : + // complexSelector; + var selectors = complexSelector.split(','); + var reducedBodies = []; + + for (var j = 0, m = selectors.length; j < m; j++) { + var selector = selectors[j]; + var data = positions[selector]; + + if (data.length < 2) + continue allSelectors; + + /* jshint loopfunc: true */ + this.reduceSelector(tokens, selector, data, { + filterOut: function (idx) { + return data[idx].where < intoPosition; + }, + callback: function (token, newBody, processedCount, tokenIdx) { + if (tokenIdx === 0) + reducedBodies.push(newBody); + } + }); + + if (reducedBodies[reducedBodies.length - 1] != reducedBodies[0]) + continue allSelectors; + } + + intoToken.body = reducedBodies[0]; + reduced = true; + } + + return reduced; +}; + +AdvancedOptimizer.prototype.reduceSelector = function (tokens, selector, data, options) { + var bodies = []; + var joinsAt = []; + var splitBodies = []; + var processedTokens = []; + + for (var j = data.length - 1, m = 0; j >= 0; j--) { + if (options.filterOut(j, bodies)) + continue; + + var where = data[j].where; + var token = tokens[where]; + var body = token.body; + // TODO: broken due to joining/splitting + bodies.push(body.join(';')); + splitBodies.push(body); + processedTokens.push(where); + } + + for (j = 0, m = bodies.length; j < m; j++) { + if (bodies[j].length > 0) + joinsAt.push((joinsAt[j - 1] || 0) + splitBodies[j].length); + } + + // TODO: broken due to joining/splitting + var optimizedBody = this.propertyOptimizer.process(bodies.join(';'), joinsAt, true, selector).split(';'); + var optimizedProperties = optimizedBody; + + var processedCount = processedTokens.length; + var propertyIdx = optimizedProperties.length - 1; + var tokenIdx = processedCount - 1; + + while (tokenIdx >= 0) { + if ((tokenIdx === 0 || splitBodies[tokenIdx].join(';').indexOf(optimizedProperties[propertyIdx]) > -1) && propertyIdx > -1) { + propertyIdx--; + continue; + } + + var newBody = optimizedProperties.splice(propertyIdx + 1); + options.callback(tokens[processedTokens[tokenIdx]], newBody, processedCount, tokenIdx); + + tokenIdx--; + } +}; + +AdvancedOptimizer.prototype.noChanges = function () { + return this.minificationsMade.length > 4 && + this.minificationsMade[0] === false && + this.minificationsMade[1] === false; +}; + +function optimizeProperties(tokens, propertyOptimizer) { + for (var i = 0, l = tokens.length; i < l; i++) { + var token = tokens[i]; + + if (token.selector) { + // TODO: broken due to joining/splitting + token.body = propertyOptimizer.process(token.body.join(';'), false, false, token.selector.join(',')).split(';'); + } else if (token.block) { + optimizeProperties(token.body, propertyOptimizer); + } + } +} + +AdvancedOptimizer.prototype.optimize = function (tokens) { + optimizeProperties(tokens, this.propertyOptimizer); + + // Run until 2 last operations do not yield any changes + while (true) { + if (this.noChanges()) + break; + this.removeDuplicates(tokens); + + if (this.noChanges()) + break; + this.mergeAdjacent(tokens); + + if (this.noChanges()) + break; + this.reduceNonAdjacent(tokens); + } +}; + +module.exports = AdvancedOptimizer; diff --git a/lib/selectors/optimizers/clean-up.js b/lib/selectors/optimizers/clean-up.js new file mode 100644 index 00000000..27b9d8ba --- /dev/null +++ b/lib/selectors/optimizers/clean-up.js @@ -0,0 +1,28 @@ +var CleanUp = { + selectors: function (selectors) { + var plain = []; + + for (var i = 0, l = selectors.length; i < l; i++) { + var reduced = selectors[i] + .trim() + .replace(/\s*([>\+\~])\s*/g, '$1') + .replace(/\*([:#\.\[])/g, '$1') + .replace(/\[([^\]]+)\]/g, function (match, value) { return '[' + value.replace(/\s/g, '') + ']'; }) + .replace(/^(\:first\-child)?\+html/, '*$1+html'); + + if (plain.indexOf(reduced) == -1) + plain.push(reduced); + } + + return plain.sort(); + }, + + block: function (block) { + return block + .replace(/(\s{2,}|\s)/g, ' ') + .replace(/(,|:|\() /g, '$1') + .replace(/ \)/g, ')'); + } +}; + +module.exports = CleanUp; diff --git a/lib/selectors/optimizers/simple.js b/lib/selectors/optimizers/simple.js new file mode 100644 index 00000000..5ca68f71 --- /dev/null +++ b/lib/selectors/optimizers/simple.js @@ -0,0 +1,26 @@ +var PropertyOptimizer = require('../../properties/optimizer'); +var CleanUp = require('./clean-up'); + +function SimpleOptimizer(options, context) { + this.options = options; + this.propertyOptimizer = new PropertyOptimizer(this.options.compatibility, this.options.aggressiveMerging, context); +} + +function minify(tokens) { + for (var i = 0, l = tokens.length; i < l; i++) { + var token = tokens[i]; + + if (token.selector) { + token.selector = CleanUp.selectors(token.selector); + } else if (token.block) { + token.block = CleanUp.block(token.block); + minify(token.body); + } + } +} + +SimpleOptimizer.prototype.optimize = function(tokens) { + minify(tokens); +}; + +module.exports = SimpleOptimizer; diff --git a/test/integration-test.js b/test/integration-test.js index 3e631b3d..c65f3560 100644 --- a/test/integration-test.js +++ b/test/integration-test.js @@ -238,26 +238,6 @@ vows.describe('integration tests').addBatch({ ] }, { keepBreaks: true, keepSpecialComments: 0 }), 'selectors': cssContext({ - 'remove spaces around selectors': [ - 'div + span > em{display:block}', - 'div+span>em{display:block}' - ], - 'not remove spaces for pseudo-classes': [ - 'div :first-child{display:block}', - 'div :first-child{display:block}' - ], - 'strip universal selector from id and class selectors': [ - '* > *#id > *.class{display:block}', - '*>#id>.class{display:block}' - ], - 'strip universal selector from attribute selectors': [ - '*:first-child > *[data-id]{display:block}', - ':first-child>[data-id]{display:block}' - ], - 'not strip standalone universal selector': [ - 'label ~ * + span{display:block}', - 'label~*+span{display:block}' - ], 'not expand + in selectors mixed with calc methods': [ 'div{width:calc(50% + 3em)}div + div{width:100%}div:hover{width:calc(50% + 4em)}* > div {border:1px solid #f0f}', 'div{width:calc(50% + 3em)}div+div{width:100%}div:hover{width:calc(50% + 4em)}*>div{border:1px solid #f0f}' diff --git a/test/selectors/optimizer-test.js b/test/selectors/optimizer-test.js new file mode 100644 index 00000000..18c25605 --- /dev/null +++ b/test/selectors/optimizer-test.js @@ -0,0 +1,189 @@ +var vows = require('vows'); +var assert = require('assert'); +var SelectorsOptimizer = require('../../lib/selectors/optimizer'); + +function optimizerContext(group, specs, options) { + var context = {}; + + function optimized(target) { + return function (source) { + assert.equal(new SelectorsOptimizer(options).process(source), target); + }; + } + + for (var name in specs) { + context[group + ' - ' + name] = { + topic: specs[name][0], + optimized: optimized(specs[name][1]) + }; + } + + return context; +} + +vows.describe(SelectorsOptimizer) + .addBatch( + optimizerContext('selectors', { + 'whitespace - heading & trailing': [ + ' a {color:red}', + 'a{color:red}' + ], + 'whitespace - descendant selector': [ + 'div > a{color:red}', + 'div>a{color:red}' + ], + 'whitespace - next selector': [ + 'div + a{color:red}', + 'div+a{color:red}' + ], + 'whitespace - sibling selector': [ + 'div ~ a{color:red}', + 'div~a{color:red}' + ], + 'whitespace - pseudo classes': [ + 'div :first-child{color:red}', + 'div :first-child{color:red}' + ], + 'whitespace - line breaks': [ + '\r\ndiv\n{color:red}', + 'div{color:red}' + ], + 'whitespace - tabs': [ + 'div\t\t{color:red}', + 'div{color:red}' + ], + 'universal selector - id, class, and property': [ + '* > *#id > *.class > *[property]{color:red}', + '*>#id>.class>[property]{color:red}' + ], + 'universal selector - pseudo': [ + '*:first-child{color:red}', + ':first-child{color:red}' + ], + 'universal selector - standalone': [ + 'label ~ * + span{color:red}', + 'label~*+span{color:red}' + ], + 'order': [ + 'b,div,a{color:red}', + 'a,b,div{color:red}' + ], + 'duplicates': [ + 'a,div,.class,.class,a ,div > a{color:red}', + '.class,a,div,div>a{color:red}' + ], + 'mixed': [ + ' label ~ \n* + span , div>*.class, section\n\n{color:red}', + 'div>.class,label~*+span,section{color:red}' + ], + 'calc': [ + 'a{width:-moz-calc(100% - 1em);width:calc(100% - 1em)}', + 'a{width:-moz-calc(100% - 1em);width:calc(100% - 1em)}' + ] + }) + ) + .addBatch( + optimizerContext('properties', { + 'empty body': [ + 'a{}', + '' + ], + 'whitespace body': [ + 'a{ \n }', + '' + ], + }) + ) + .addBatch( + optimizerContext('@media', { + 'empty': [ + '@media (min-width:980px){}', + '' + ], + 'whitespace': [ + ' @media ( min-width: 980px ){}', + '' + ], + 'body': [ + '@media (min-width:980px){\na\n{color:red}}', + '@media (min-width:980px){a{color:red}}' + ], + 'multiple': [ + '@media screen, print, (min-width:980px){a{color:red}}', + '@media screen,print,(min-width:980px){a{color:red}}' + ], + 'nested once': [ + '@media screen { @media print { a{color:red} } }', + '@media screen{@media print{a{color:red}}}' + ], + 'nested twice': [ + '@media screen { @media print { @media (min-width:980px) { a{color:red} } } }', + '@media screen{@media print{@media (min-width:980px){a{color:red}}}}' + ] + }) + ) + .addBatch( + optimizerContext('advanced on & aggressive merging on', { + 'repeated' : [ + 'a{color:red;color:red}', + 'a{color:red}' + ], + 'duplicates - same context': [ + 'a{color:red}div{color:blue}a{color:red}', + 'div{color:blue}a{color:red}' + ], + 'duplicates - different contexts': [ + 'a{color:red}div{color:blue}@media screen{a{color:red}}', + 'a{color:red}div{color:blue}@media screen{a{color:red}}' + ], + 'adjacent': [ + 'a{color:red}a{display:block;width:100px}div{color:#fff}', + 'a{color:red;display:block;width:100px}div{color:#fff}' + ], + 'non-adjacent': [ + 'a{color:red;display:block}.one{font-size:12px}a{color:#fff;margin:2px}', + 'a{display:block}.one{font-size:12px}a{color:#fff;margin:2px}' + ], + 'non-adjacent with multi selectors': [ + 'a{padding:10px;margin:0;color:red}.one{color:red}a,p{color:red;padding:0}', + 'a{margin:0}.one{color:red}a,p{color:red;padding:0}' + ] + }, { noAdvanced: false, aggressiveMerging: true }) + ) + .addBatch( + optimizerContext('advanced on & aggressive merging off', { + 'repeated' : [ + 'a{color:red;color:red}', + 'a{color:red}' + ], + 'non-adjacent with multi selectors': [ + 'a{padding:10px;margin:0;color:red}.one{color:red}a,p{color:red;padding:0}', + 'a{padding:10px;margin:0}.one{color:red}a,p{color:red;padding:0}' + ] + }, { noAdvanced: false, aggressiveMerging: false }) + ) + .addBatch( + optimizerContext('advanced off', { + 'repeated' : [ + 'a{color:red;color:red}', + 'a{color:red;color:red}' + ], + 'duplicates - same context': [ + 'a{color:red}div{color:blue}a{color:red}', + 'a{color:red}div{color:blue}a{color:red}' + ], + 'duplicates - different contexts': [ + 'a{color:red}div{color:blue}@media screen{a{color:red}}', + 'a{color:red}div{color:blue}@media screen{a{color:red}}' + ], + 'adjacent': [ + 'a{color:red}a{display:block;width:100px}div{color:#fff}', + 'a{color:red}a{display:block;width:100px}div{color:#fff}' + ], + 'non-adjacent': [ + 'a{color:red;display:block}.one{font-size:12px}a{color:#fff;margin:2px}', + 'a{color:red;display:block}.one{font-size:12px}a{color:#fff;margin:2px}' + ] + }, { noAdvanced: true }) + ) + .export(module); diff --git a/test/selectors/optimizers/simple-test.js b/test/selectors/optimizers/simple-test.js new file mode 100644 index 00000000..de1717b3 --- /dev/null +++ b/test/selectors/optimizers/simple-test.js @@ -0,0 +1,46 @@ +var vows = require('vows'); +var assert = require('assert'); + +var Tokenizer = require('../../../lib/selectors/tokenizer'); +var SimpleOptimizer = require('../../../lib/selectors/optimizers/simple'); + +function selectorContext(specs) { + var context = {}; + + function optimized(selectors) { + return function (source) { + var tokens = new Tokenizer().toTokens(source); + new SimpleOptimizer({}).optimize(tokens); + + assert.deepEqual(tokens[0].selector, selectors); + }; + } + + for (var name in specs) { + context['selector - ' + name] = { + topic: specs[name][0], + optimized: optimized(specs[name][1]) + }; + } + + return context; +} + +vows.describe(SimpleOptimizer) + .addBatch( + selectorContext({ + 'optimized': [ + 'a{}', + ['a'] + ], + 'whitespace': [ + ' div > span{}', + ['div>span'] + ], + 'line breaks': [ + ' div >\n\r\n span{}', + ['div>span'] + ] + }) + ) + .export(module);