var processableInfo = require('./processable'); var overrideCompactor = require('./override-compactor'); var shorthandCompactor = require('./shorthand-compactor'); module.exports = function Optimizer(compatibility, aggressiveMerging, context) { var overridable = { 'animation-delay': ['animation'], 'animation-direction': ['animation'], 'animation-duration': ['animation'], 'animation-fill-mode': ['animation'], 'animation-iteration-count': ['animation'], 'animation-name': ['animation'], 'animation-play-state': ['animation'], 'animation-timing-function': ['animation'], '-moz-animation-delay': ['-moz-animation'], '-moz-animation-direction': ['-moz-animation'], '-moz-animation-duration': ['-moz-animation'], '-moz-animation-fill-mode': ['-moz-animation'], '-moz-animation-iteration-count': ['-moz-animation'], '-moz-animation-name': ['-moz-animation'], '-moz-animation-play-state': ['-moz-animation'], '-moz-animation-timing-function': ['-moz-animation'], '-o-animation-delay': ['-o-animation'], '-o-animation-direction': ['-o-animation'], '-o-animation-duration': ['-o-animation'], '-o-animation-fill-mode': ['-o-animation'], '-o-animation-iteration-count': ['-o-animation'], '-o-animation-name': ['-o-animation'], '-o-animation-play-state': ['-o-animation'], '-o-animation-timing-function': ['-o-animation'], '-webkit-animation-delay': ['-webkit-animation'], '-webkit-animation-direction': ['-webkit-animation'], '-webkit-animation-duration': ['-webkit-animation'], '-webkit-animation-fill-mode': ['-webkit-animation'], '-webkit-animation-iteration-count': ['-webkit-animation'], '-webkit-animation-name': ['-webkit-animation'], '-webkit-animation-play-state': ['-webkit-animation'], '-webkit-animation-timing-function': ['-webkit-animation'], 'background-clip': ['background'], 'background-origin': ['background'], 'border-color': ['border'], 'border-style': ['border'], 'border-width': ['border'], 'border-bottom': ['border'], 'border-bottom-color': ['border-bottom', 'border-color', 'border'], 'border-bottom-style': ['border-bottom', 'border-style', 'border'], 'border-bottom-width': ['border-bottom', 'border-width', 'border'], 'border-left': ['border'], 'border-left-color': ['border-left', 'border-color', 'border'], 'border-left-style': ['border-left', 'border-style', 'border'], 'border-left-width': ['border-left', 'border-width', 'border'], 'border-right': ['border'], 'border-right-color': ['border-right', 'border-color', 'border'], 'border-right-style': ['border-right', 'border-style', 'border'], 'border-right-width': ['border-right', 'border-width', 'border'], 'border-top': ['border'], 'border-top-color': ['border-top', 'border-color', 'border'], 'border-top-style': ['border-top', 'border-style', 'border'], 'border-top-width': ['border-top', 'border-width', 'border'], 'font-family': ['font'], 'font-size': ['font'], 'font-style': ['font'], 'font-variant': ['font'], 'font-weight': ['font'], 'list-style-image': ['list-style'], 'list-style-position': ['list-style'], 'list-style-type': ['list-style'], 'margin-bottom': ['margin'], 'margin-left': ['margin'], 'margin-right': ['margin'], 'margin-top': ['margin'], 'outline-color': ['outline'], 'outline-style': ['outline'], 'outline-width': ['outline'], 'padding-bottom': ['padding'], 'padding-left': ['padding'], 'padding-right': ['padding'], 'padding-top': ['padding'], 'transition-delay': ['transition'], 'transition-duration': ['transition'], 'transition-property': ['transition'], 'transition-timing-function': ['transition'], '-moz-transition-delay': ['-moz-transition'], '-moz-transition-duration': ['-moz-transition'], '-moz-transition-property': ['-moz-transition'], '-moz-transition-timing-function': ['-moz-transition'], '-o-transition-delay': ['-o-transition'], '-o-transition-duration': ['-o-transition'], '-o-transition-property': ['-o-transition'], '-o-transition-timing-function': ['-o-transition'], '-webkit-transition-delay': ['-webkit-transition'], '-webkit-transition-duration': ['-webkit-transition'], '-webkit-transition-property': ['-webkit-transition'], '-webkit-transition-timing-function': ['-webkit-transition'] }; var IE_BACKSLASH_HACK = '\\9'; var overrides = {}; for (var granular in overridable) { for (var i = 0; i < overridable[granular].length; i++) { var coarse = overridable[granular][i]; var list = overrides[coarse]; if (list) list.push(granular); else overrides[coarse] = [granular]; } } var tokenize = function(body, selector) { var tokens = body.split(';'); var keyValues = []; if (tokens.length === 0 || (tokens.length == 1 && tokens[0].indexOf(IE_BACKSLASH_HACK) == -1 && tokens[0][tokens[0].length - 1] != ':')) return; for (var i = 0, l = tokens.length; i < l; i++) { var token = tokens[i]; if (token === '') continue; var firstColon = token.indexOf(':'); var property = token.substring(0, firstColon); var value = token.substring(firstColon + 1); if (value === '') { context.warnings.push('Empty property \'' + property + '\' inside \'' + selector + '\' selector. Ignoring.'); continue; } keyValues.push([ property, value, token.indexOf('!important') > -1, token.indexOf(IE_BACKSLASH_HACK, firstColon + 1) === token.length - IE_BACKSLASH_HACK.length ]); } return keyValues; }; var optimize = function(tokens, allowAdjacent) { var merged = []; var properties = []; var lastProperty = null; var rescanTrigger = {}; var removeOverridenBy = function(property, isImportant) { var overrided = overrides[property]; for (var i = 0, l = overrided.length; i < l; i++) { for (var j = 0; j < properties.length; j++) { if (properties[j] != overrided[i] || (merged[j][2] && !isImportant)) continue; merged.splice(j, 1); properties.splice(j, 1); j -= 1; } } }; var mergeablePosition = function(position) { if (allowAdjacent === false || allowAdjacent === true) return allowAdjacent; return allowAdjacent.indexOf(position) > -1; }; tokensLoop: for (var i = 0, l = tokens.length; i < l; i++) { var token = tokens[i]; var property = token[0]; var value = token[1]; var isImportant = token[2]; var isIEHack = token[3]; var _property = (property == '-ms-filter' || property == 'filter') ? (lastProperty == 'background' || lastProperty == 'background-image' ? lastProperty : property) : property; var toOverridePosition = 0; if (!compatibility && isIEHack) continue; // comment is necessary - we assume that if two properties are one after another // then it is intentional way of redefining property which may not be widely supported // e.g. a{display:inline-block;display:-moz-inline-box} // however if `mergeablePosition` yields true then the rule does not apply // (e.g merging two adjacent selectors: `a{display:block}a{display:block}`) if (aggressiveMerging && _property != lastProperty || mergeablePosition(i)) { while (true) { toOverridePosition = properties.indexOf(_property, toOverridePosition); if (toOverridePosition == -1) break; var lastToken = merged[toOverridePosition]; var wasImportant = lastToken[2]; var wasIEHack = lastToken[3]; if (wasImportant && !isImportant) continue tokensLoop; if (compatibility && !wasIEHack && isIEHack) break; var _info = processableInfo.processable[_property]; if (!isIEHack && !wasIEHack && _info && _info.canOverride && !_info.canOverride(tokens[toOverridePosition][1], value)) break; merged.splice(toOverridePosition, 1); properties.splice(toOverridePosition, 1); } } merged.push(token); properties.push(_property); // certain properties (see values of `overridable`) should trigger removal of // more granular properties (see keys of `overridable`) if (rescanTrigger[_property]) removeOverridenBy(_property, isImportant); // add rescan triggers - if certain property appears later in the list a rescan needs // to be triggered, e.g 'border-top' triggers a rescan after 'border-top-width' and // 'border-top-color' as they can be removed for (var j = 0, list = overridable[_property] || [], m = list.length; j < m; j++) rescanTrigger[list[j]] = true; lastProperty = _property; } return merged; }; var rebuild = function(tokens) { var flat = []; for (var i = 0, l = tokens.length; i < l; i++) { flat.push(tokens[i][0] + ':' + tokens[i][1]); } return flat.join(';'); }; var compact = function (input) { var processable = processableInfo.processable; var Token = processableInfo.Token; var tokens = Token.tokenize(input); tokens = overrideCompactor.compactOverrides(tokens, processable, Token, compatibility); tokens = shorthandCompactor.compactShorthands(tokens, false, processable, Token); tokens = shorthandCompactor.compactShorthands(tokens, true, processable, Token); return Token.detokenize(tokens); }; return { process: function(body, allowAdjacent, skipCompacting, selector) { var result = body; var tokens = tokenize(body, selector); if (tokens) { var optimized = optimize(tokens, allowAdjacent); result = rebuild(optimized); } if (!skipCompacting && processableInfo.implementedFor.test(result)) { result = compact(result); } return result; } }; };