mirror of
https://github.com/wahyd4/cdnjs.git
synced 2026-08-11 05:46:04 +10:00
342 lines
9.5 KiB
JavaScript
342 lines
9.5 KiB
JavaScript
var Tokenizer = require('./tokenizer');
|
|
var PropertyOptimizer = require('../properties/optimizer');
|
|
|
|
module.exports = function Optimizer(data, context, options) {
|
|
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);
|
|
|
|
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);
|
|
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);
|
|
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);
|
|
} 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);
|
|
}
|
|
};
|
|
};
|