* 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.
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;
--- /dev/null
+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;
--- /dev/null
+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;
--- /dev/null
+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;
]
}, { 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}'
--- /dev/null
+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);
--- /dev/null
+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);