123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- var Tokenizer = require('./tokenizer');
- var PropertyOptimizer = require('../properties/optimizer');
-
- module.exports = function Optimizer(data, context, options) {
- var specialSelectors = {
- '*': /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:dir\([a-z-]*\)|:first(?![a-z-])|:fullscreen|:left|:read-only|:read-write|:right)/,
- '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() {
- var tokenized = new Tokenizer(data, context).process();
- optimize(tokenized);
- return rebuild(tokenized);
- }
- };
- };
|