* We'll be able to hold more information about tokens now.
var overrideCompactor = require('./override-compactor');
var shorthandCompactor = require('./shorthand-compactor');
+function valueMapper(object) { return object.value; }
+
module.exports = function Optimizer(compatibility, aggressiveMerging, context) {
var overridable = {
'animation-delay': ['animation'],
var keyValues = [];
for (var i = 0, l = body.length; i < l; i++) {
- var token = body[i];
+ var token = body[i].value;
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.');
+ context.warnings.push('Empty property \'' + property + '\' inside \'' + selector.map(valueMapper).join(',') + '\' selector. Ignoring.');
continue;
}
var rebuild = function(tokens) {
var flat = [];
+ var eligibleForCompacting = false;
for (var i = 0, l = tokens.length; i < l; i++) {
- flat.push(tokens[i][0] + ':' + tokens[i][1]);
+ if (!eligibleForCompacting && processableInfo.implementedFor.test(tokens[i][0]))
+ eligibleForCompacting = true;
+
+ flat.push({ value: tokens[i][0] + ':' + tokens[i][1] });
}
- return flat;
+ return {
+ value: flat,
+ compactFurther: eligibleForCompacting
+ };
};
var compact = function (input) {
var optimized = optimize(tokenized, allowAdjacent);
var rebuilt = rebuild(optimized);
- return compactProperties && processableInfo.implementedFor.test(rebuilt) ?
- compact(rebuilt) :
- rebuilt;
+ return compactProperties && rebuilt.compactFurther ?
+ compact(rebuilt.value) :
+ rebuilt.value;
}
};
};
return canOverride.sameFunctionOrValue(val1, val2);
},
border: function(val1, val2) {
- var brokenUp1 = breakUp.border(Token.tokenizeOne(val1));
- var brokenUp2 = breakUp.border(Token.tokenizeOne(val2));
+ var brokenUp1 = breakUp.border(Token.tokenizeOne({ value: val1 }));
+ var brokenUp2 = breakUp.border(Token.tokenizeOne({ value: val2 }));
return canOverride.color(brokenUp1[2].value, brokenUp2[2].value);
}
// Parses one CSS property declaration into a token
Token.tokenizeOne = function (fullProp) {
// Find first colon
- var colonPos = fullProp.indexOf(':');
+ var colonPos = fullProp.value.indexOf(':');
if (colonPos < 0) {
// This property doesn't have a colon, it's invalid. Let's keep it intact anyway.
- return new Token('', fullProp);
+ return new Token('', fullProp.value);
}
// Parse parts of the property
- var prop = fullProp.substr(0, colonPos).trim();
- var value = fullProp.substr(colonPos + 1).trim();
+ var prop = fullProp.value.substr(0, colonPos).trim();
+ var value = fullProp.value.substr(colonPos + 1).trim();
var isImportant = false;
var importantPos = value.indexOf(important);
continue;
}
- result.push(t.prop + ':' + t.value + (t.isImportant ? important : ''));
+ result.push({ value: t.prop + ':' + t.value + (t.isImportant ? important : '') });
}
return result;
this.context = context || {};
}
+function valueMapper (object) { return object.value; }
+
function rebuild(tokens, keepBreaks, isFlatBlock) {
var joinCharacter = isFlatBlock ? ';' : (keepBreaks ? lineBreak : '');
var parts = [];
+ var body;
+ var selector;
for (var i = 0, l = tokens.length; i < l; i++) {
var token = tokens[i];
- if (typeof token === 'string') {
- parts.push(token);
+ if (token.kind === 'text') {
+ parts.push(token.value);
continue;
}
// TODO: broken due to joining/splitting
- if (token.body.length === 0 || (token.body.length == 1 && token.body[0] === ''))
+ if (token.body && (token.body.length === 0 || (token.body.length == 1 && token.body[0].value === '')))
continue;
- if (token.block) {
- var body = rebuild(token.body, keepBreaks, token.isFlatBlock);
+ if (token.kind == 'block') {
+ body = token.isFlatBlock ?
+ token.body.map(valueMapper).join(';') :
+ rebuild(token.body, keepBreaks, token.isFlatBlock);
if (body.length > 0)
- parts.push(token.block + '{' + body + '}');
+ parts.push(token.value + '{' + body + '}');
} else {
- parts.push(token.selector.join(',') + '{' + token.body.join(';') + '}');
+ selector = token.value.map(valueMapper).join(',');
+ body = token.body.map(valueMapper).join(';');
+ parts.push(selector + '{' + body + '}');
}
}
this.propertyOptimizer = new PropertyOptimizer(this.options.compatibility, this.options.aggressiveMerging, context);
}
+function valueMapper(object) { return object.value; }
+
AdvancedOptimizer.prototype.isSpecial = function (selector) {
return this.options.compatibility.selectors.special.test(selector);
};
for (var i = 0, l = tokens.length; i < l; i++) {
var token = tokens[i];
- if (typeof token == 'string' || token.block)
+ if (token.kind != 'selector')
continue;
- var id = token.body.join(';') + '@' + token.selector.join(',');
+ var id = token.body.map(valueMapper).join(';') + '@' + token.value.map(valueMapper).join(',');
var alreadyMatched = matched[id];
if (alreadyMatched) {
for (var i = 0, l = tokens.length; i < l; i++) {
var token = tokens[i];
- if (typeof token == 'string' || token.block)
+ if (token.kind != 'selector')
continue;
// TODO: broken due to joining/splitting
- if (lastToken.selector && token.selector.join(',') == lastToken.selector.join(',')) {
+ if (lastToken.kind == 'selector' && token.value.map(valueMapper).join(',') == lastToken.value.map(valueMapper).join(',')) {
var joinAt = [lastToken.body.length];
- lastToken.body = this.propertyOptimizer.process(token.selector, lastToken.body.concat(token.body), joinAt, true);
+ lastToken.body = this.propertyOptimizer.process(token.value, lastToken.body.concat(token.body), joinAt, true);
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));
+ } else if (lastToken.body && token.body.map(valueMapper).join(';') == lastToken.body.map(valueMapper).join(';') && !this.isSpecial(token.value.map(valueMapper).join(',')) && !this.isSpecial(lastToken.value.map(valueMapper).join(','), this.options)) {
+ lastToken.value = CleanUp.selectors(lastToken.value.concat(token.value));
forRemoval.push(i);
} else {
lastToken = token;
for (var i = tokens.length - 1; i >= 0; i--) {
var token = tokens[i];
- if (typeof token == 'string' || token.block)
+ if (token.kind != 'selector')
continue;
- var complexSelector = token.selector;
- var selectors = complexSelector.length > 1 && !this.isSpecial(complexSelector.join(','), this.options) ?
- [complexSelector.join(',')].concat(complexSelector) :
- [complexSelector.join(',')];
+ var complexSelector = token.value;
+ var selectors = complexSelector.length > 1 && !this.isSpecial(complexSelector.map(valueMapper).join(','), this.options) ?
+ [complexSelector.map(valueMapper).join(',')].concat(complexSelector.map(valueMapper)) :
+ [complexSelector.map(valueMapper).join(',')];
for (var j = 0, m = selectors.length; j < m; j++) {
var selector = selectors[j];
// TODO: broken due to joining/splitting
candidates[selector].push({
where: i,
- partial: selector != complexSelector.join(',')
+ partial: selector != complexSelector.map(valueMapper).join(',')
});
}
}
},
callback: function (token, newBody, processedCount, tokenIdx) {
if (tokenIdx === 0)
- reducedBodies.push(newBody.join(';'));
+ reducedBodies.push(newBody.map(valueMapper).join(';'));
}
});
continue allSelectors;
}
- intoToken.body = reducedBodies[0].split(';');
+ intoToken.body = reducedBodies[0].split(';').map(function (property) {
+ return { value: property };
+ });
reduced = true;
}
var body = token.body;
bodies = bodies.concat(body);
- splitBodies.push(body);
+ splitBodies.push(body.map(valueMapper));
processedTokens.push(where);
}
var tokenIdx = processedCount - 1;
while (tokenIdx >= 0) {
- if ((tokenIdx === 0 || splitBodies[tokenIdx].indexOf(optimizedProperties[propertyIdx]) > -1) && propertyIdx > -1) {
+ if ((tokenIdx === 0 || (optimizedProperties[propertyIdx] && splitBodies[tokenIdx].indexOf(optimizedProperties[propertyIdx].value) > -1)) && propertyIdx > -1) {
propertyIdx--;
continue;
}
for (var i = 0, l = tokens.length; i < l; i++) {
var token = tokens[i];
- if (token.selector) {
- token.body = propertyOptimizer.process(token.selector, token.body, false, true);
- } else if (token.block) {
+ if (token.kind == 'selector') {
+ token.body = propertyOptimizer.process(token.value, token.body, false, true);
+ } else if (token.kind == 'block') {
optimizeProperties(token.body, propertyOptimizer);
}
}
function _optimize(tokens) {
tokens.forEach(function (token) {
- if (token.block)
+ if (token.kind == 'block')
_optimize(token.body);
});
var plain = [];
for (var i = 0, l = selectors.length; i < l; i++) {
- var selector = selectors[i];
+ var selector = selectors[i].value;
var reduced = selector.replace(/\s*([>\+\~])\s*/g, '$1');
if (selector.indexOf('*') > -1) {
plain.push(reduced);
}
- return plain.sort();
+ return plain.sort().map(function (selector) {
+ return { value: selector };
+ });
},
block: function (block) {
return;
var supported = [];
- for (var i = 0, l = token.selector.length; i < l; i++) {
- var selector = token.selector[i];
+ for (var i = 0, l = token.value.length; i < l; i++) {
+ var selector = token.value[i];
- if (selector.indexOf('*+html ') === -1 && selector.indexOf('*:first-child+html ') === -1)
+ if (selector.value.indexOf('*+html ') === -1 && selector.value.indexOf('*:first-child+html ') === -1)
supported.push(selector);
}
- token.selector = supported;
+ token.value = supported;
}
var valueMinifiers = {
var reduced = [];
for (var i = 0, l = body.length; i < l; i++) {
- var token = body[i];
+ var token = body[i].value;
var firstColon = token.indexOf(':');
var property = token.substring(0, firstColon);
var value = token.substring(firstColon + 1);
value = multipleZerosMinifier(property, value);
value = colorMininifier(property, value, options.compatibility);
- reduced.push(property + ':' + value + (important ? '!important' : ''));
+ reduced.push({ value: property + ':' + value + (important ? '!important' : '') });
}
return reduced;
if (!token)
break;
- if (token.selector) {
- token.selector = CleanUp.selectors(token.selector);
+ if (token.kind == 'selector') {
+ token.value = CleanUp.selectors(token.value);
removeUnsupported(token, self.options.compatibility);
- if (token.selector.length === 0) {
+ if (token.value.length === 0) {
tokens.splice(i, 1);
i--;
continue;
}
token.body = reduce(token.body, self.options);
- } else if (token.block) {
- token.block = CleanUp.block(token.block);
+ } else if (token.kind == 'block') {
+ token.value = CleanUp.block(token.value);
if (token.isFlatBlock)
token.body = reduce(token.body, self.options);
else
_optimize(token.body);
- } else if (typeof token === 'string') {
- if (CHARSET_REGEXP.test(token)) {
- if (hasCharset || token.indexOf(CHARSET_TOKEN) == -1) {
+ } else if (token.kind == 'text') {
+ if (CHARSET_REGEXP.test(token.value)) {
+ if (hasCharset || token.value.indexOf(CHARSET_TOKEN) == -1) {
tokens.splice(i, 1);
i++;
} else {
hasCharset = true;
tokens.splice(i, 1);
- tokens.unshift(token.replace(CHARSET_REGEXP, CHARSET_TOKEN));
+ tokens.unshift({ kind: 'text', value: token.value.replace(CHARSET_REGEXP, CHARSET_TOKEN) });
}
}
}
.replace(TRAILING_SEMICOLON, '');
return extracted.length > 0 ?
- extracted.split(';') :
+ extracted.split(';').map(function (property) { return { value: property }; }) :
[];
}
.replace(WHITESPACE_COMMA, ',')
.trim();
- return new Splitter(',').split(extracted);
+ return new Splitter(',').split(extracted).map(function (selector) {
+ return { value: selector };
+ });
}
function extractBlock(string) {
if (!next) {
var whatsLeft = context.chunk.substring(context.cursor);
if (whatsLeft.length > 0) {
- tokenized.push(whatsLeft);
+ tokenized.push({ kind: 'text', value: whatsLeft });
context.cursor += whatsLeft.length;
}
break;
nextEnd = chunk.indexOf(';', nextSpecial + 1);
var single = extractBlock(chunk.substring(context.cursor, nextEnd + 1));
- tokenized.push(single);
+ tokenized.push({ kind: 'text', value: single });
context.cursor = nextEnd + 1;
} else {
context.mode = oldMode;
- tokenized.push({ block: block, body: specialBody, isFlatBlock: isFlat });
+ tokenized.push({ kind: 'block', value: block, body: specialBody, isFlatBlock: isFlat });
}
} else if (what == 'escape') {
nextEnd = chunk.indexOf('__', nextSpecial + 1);
var escaped = chunk.substring(context.cursor, nextEnd + 2);
- tokenized.push(escaped);
+ tokenized.push({ kind: 'text', value: escaped });
context.cursor = nextEnd + 2;
} else if (what == 'bodyStart') {
context.mode = oldMode;
- tokenized.push({ selector: selector, body: body });
+ tokenized.push({ kind: 'selector', value: selector, body: body });
} else if (what == 'bodyEnd') {
// extra closing brace at the top level can be safely ignored
if (context.mode == 'top') {
var tokens = new Tokenizer().toTokens(source);
new SimpleOptimizer(options).optimize(tokens);
- assert.deepEqual(tokens[0] ? tokens[0].selector : null, selectors);
+ assert.deepEqual(tokens[0] ? tokens[0].value : null, selectors);
};
}
return function (source) {
var tokens = new Tokenizer().toTokens(source);
new SimpleOptimizer(options).optimize(tokens);
+ var value = tokens[0].body.map(function (property) { return property.value; });
- assert.deepEqual(tokens[0].body, selectors);
+ assert.deepEqual(value, selectors);
};
}
selectorContext('default', {
'optimized': [
'a{}',
- ['a']
+ [{ value: 'a' }]
],
'whitespace': [
' div > span{}',
- ['div>span']
+ [{ value: 'div>span' }]
],
'line breaks': [
' div >\n\r\n span{}',
- ['div>span']
+ [{ value: 'div>span' }]
],
'+html': [
'*+html .foo{display:inline}',
],
'+html - complex': [
'*+html .foo,.bar{display:inline}',
- ['.bar']
+ [{ value: '.bar' }]
]
}, { compatibility: 'ie8' })
)
selectorContext('ie7', {
'+html': [
'*+html .foo{display:inline}',
- ['*+html .foo']
+ [{ value: '*+html .foo' }]
],
'+html - complex': [
'*+html .foo,.bar{display:inline}',
- ['*+html .foo', '.bar']
+ [{ value: '*+html .foo' }, { value: '.bar' }]
]
}, { compatibility: 'ie7' })
)
var assert = require('assert');
var Tokenizer = require('../../lib/selectors/tokenizer');
-function tokenizerContext(config) {
+function tokenizerContext(name, specs) {
var ctx = {};
function tokenized(target) {
};
}
- for (var test in config) {
+ for (var test in specs) {
ctx[test] = {
- topic: config[test][0],
- tokenized: tokenized(config[test][1])
+ topic: specs[test][0],
+ tokenized: tokenized(specs[test][1])
};
}
vows.describe(Tokenizer)
.addBatch(
- tokenizerContext({
+ tokenizerContext('basic', {
'no content': [
'',
[]
],
'an escaped content': [
'__ESCAPED_COMMENT_CLEAN_CSS0__',
- ['__ESCAPED_COMMENT_CLEAN_CSS0__']
+ [{
+ kind: 'text',
+ value: '__ESCAPED_COMMENT_CLEAN_CSS0__'
+ }]
],
'an empty selector': [
'a{}',
- [{ selector: ['a'], body: [] }]
+ [{
+ kind: 'selector',
+ value: [{ value: 'a' }],
+ body: []
+ }]
],
'an empty selector with whitespace': [
'a{ \n }',
- [{ selector: ['a'], body: [] }]
+ [{
+ kind: 'selector',
+ value: [{ value: 'a' }],
+ body: []
+ }]
],
'a selector': [
'a{color:red}',
- [{ selector: ['a'], body: ['color:red'] }]
+ [{
+ kind: 'selector',
+ value: [{ value: 'a' }],
+ body: [{ value: 'color:red' }]
+ }]
],
'a selector with whitespace': [
'a {color:red;\n\ndisplay : block }',
- [{ selector: ['a'], body: ['color:red', 'display:block'] }]
+ [{
+ kind: 'selector',
+ value: [{ value: 'a' }],
+ body: [
+ { value: 'color:red' },
+ { value: 'display:block'
+ }]
+ }]
],
'a selector with whitespace in functions': [
'a{color:rgba( 255, 255, 0, 0.5 )}',
- [{ selector: ['a'], body: ['color:rgba(255,255,0,0.5)'] }]
+ [{
+ kind: 'selector',
+ value: [{ value: 'a' }],
+ body: [{ value: 'color:rgba(255,255,0,0.5)' }]
+ }]
],
'a selector with empty properties': [
'a{color:red; ; ; ;}',
- [{ selector: ['a'], body: ['color:red'] }]
+ [{
+ kind: 'selector',
+ value: [{ value: 'a' }],
+ body: [{ value: 'color:red' }]
+ }]
],
'a selector with quoted attribute': [
'a[data-kind=__ESCAPED_FREE_TEXT_CLEAN_CSS0__]{color:red}',
- [{ selector: ['a[data-kind=__ESCAPED_FREE_TEXT_CLEAN_CSS0__]'], body: ['color:red'] }]
+ [{
+ kind: 'selector',
+ value: [{ value: 'a[data-kind=__ESCAPED_FREE_TEXT_CLEAN_CSS0__]' }],
+ body: [{ value: 'color:red' }]
+ }]
],
'a double selector': [
'a,\n\ndiv.class > p {color:red}',
- [{ selector: ['a', 'div.class > p'], body: ['color:red'] }]
+ [{
+ kind: 'selector',
+ value: [
+ { value: 'a' },
+ { value: 'div.class > p' }
+ ],
+ body: [{ value: 'color:red' }]
+ }]
],
'two selectors': [
'a{color:red}div{color:blue}',
[
- { selector: ['a'], body: ['color:red'] },
- { selector: ['div'], body: ['color:blue'] }
+ {
+ kind: 'selector',
+ value: [{ value: 'a' }],
+ body: [{ value: 'color:red' }]
+ },
+ {
+ kind: 'selector',
+ value: [{ value: 'div' }],
+ body: [{ value: 'color:blue' }]
+ }
]
],
'media query': [
'@media (min-width:980px){}',
- [{ block: '@media (min-width:980px)', body: [], isFlatBlock: false }]
+ [{
+ kind: 'block',
+ value: '@media (min-width:980px)',
+ body: [],
+ isFlatBlock: false
+ }]
],
'media query with selectors': [
'@media (min-width:980px){a{color:red}}',
- [{ block: '@media (min-width:980px)', body: [{ selector: ['a'], body: ['color:red'] }], isFlatBlock: false }]
+ [{
+ kind: 'block',
+ value: '@media (min-width:980px)',
+ body: [{
+ kind: 'selector',
+ value: [{ value: 'a' }],
+ body: [{ value: 'color:red' }]
+ }],
+ isFlatBlock: false
+ }]
],
'media query spanning more than one chunk': [
'@media only screen and (max-width:1319px) and (min--moz-device-pixel-ratio:1.5),only screen and (max-width:1319px) and (-moz-min-device-pixel-ratio:1.5){a{color:#000}}',
- [{ block: '@media only screen and (max-width:1319px) and (min--moz-device-pixel-ratio:1.5),only screen and (max-width:1319px) and (-moz-min-device-pixel-ratio:1.5)', body: [{ selector: ['a'], body: ['color:#000'] }], isFlatBlock: false }]
+ [{
+ kind: 'block',
+ value: '@media only screen and (max-width:1319px) and (min--moz-device-pixel-ratio:1.5),only screen and (max-width:1319px) and (-moz-min-device-pixel-ratio:1.5)',
+ body: [{
+ kind: 'selector',
+ value: [{ value: 'a' }],
+ body: [{ value: 'color:#000' }]
+ }],
+ isFlatBlock: false
+ }]
],
'font-face': [
'@font-face{font-family: fontName;font-size:12px}',
- [{ block: '@font-face', body: ['font-family:fontName', 'font-size:12px'], isFlatBlock: true }]
+ [{
+ kind: 'block',
+ value: '@font-face',
+ body: [
+ { value: 'font-family:fontName' },
+ { value: 'font-size:12px' }
+ ],
+ isFlatBlock: true
+ }]
],
'charset': [
'@charset \'utf-8\';a{color:red}',
- ['@charset \'utf-8\';', { selector: ['a'], body: ['color:red'] }]
+ [
+ {
+ kind: 'text',
+ value: '@charset \'utf-8\';'
+ },
+ {
+ kind: 'selector',
+ value: [{ value: 'a' }],
+ body: [{ value: 'color:red' }]
+ }
+ ]
],
'charset after a line break': [
'\n@charset \n\'utf-8\';',
- ['@charset \'utf-8\';']
+ [{
+ kind: 'text',
+ value: '@charset \'utf-8\';'
+ }]
],
'keyframes with quoted attribute': [
'@keyframes __ESCAPED_FREE_TEXT_CLEAN_CSS0__{}',
- [{ block: '@keyframes __ESCAPED_FREE_TEXT_CLEAN_CSS0__', body: [], isFlatBlock: false }]
+ [{
+ kind: 'block',
+ value: '@keyframes __ESCAPED_FREE_TEXT_CLEAN_CSS0__',
+ body: [],
+ isFlatBlock: false
+ }]
]
})
)