* We can avoid merging, splitting, and mapping by using token metadata directly.
* Unfortunately it means metadata has to be updated as we go.
};
var rebuild = function(tokens) {
- var flat = [];
+ var tokenized = [];
+ var list = [];
var eligibleForCompacting = false;
for (var i = 0, l = tokens.length; i < l; i++) {
if (!eligibleForCompacting && processableInfo.implementedFor.test(tokens[i][0]))
eligibleForCompacting = true;
- flat.push({ value: tokens[i][0] + ':' + tokens[i][1] });
+ var property = tokens[i][0] + ':' + tokens[i][1];
+ tokenized.push({ value: property });
+ list.push(property);
}
return {
- value: flat,
- compactFurther: eligibleForCompacting
+ compactFurther: eligibleForCompacting,
+ list: list,
+ tokenized: tokenized
};
};
var rebuilt = rebuild(optimized);
return compactProperties && rebuilt.compactFurther ?
- compact(rebuilt.value) :
- rebuilt.value;
+ compact(rebuilt.tokenized) :
+ rebuilt;
}
};
};
tokens = [tokens];
}
- var result = [];
+ var tokenized = [];
+ var list = [];
// This step takes care of putting together the components of shorthands
// NOTE: this is necessary to do for every shorthand, otherwise we couldn't remove their default values
continue;
}
- result.push({ value: t.prop + ':' + t.value + (t.isImportant ? important : '') });
+ var property = t.prop + ':' + t.value + (t.isImportant ? important : '');
+ tokenized.push({ value: property });
+ list.push(property);
}
- return result;
+ return {
+ list: list,
+ tokenized: tokenized
+ };
};
// Gets the final (detokenized) length of the given tokens
continue;
}
- // TODO: broken due to joining/splitting
+ // FIXME: broken due to joining/splitting
if (token.body && (token.body.length === 0 || (token.body.length == 1 && token.body[0].value === '')))
continue;
}
SelectorsOptimizer.prototype.process = function (data) {
- var tokens = new Tokenizer(this.context).toTokens(data);
+ var tokens = new Tokenizer(this.context, this.options.advanced).toTokens(data);
new SimpleOptimizer(this.options).optimize(tokens);
if (this.options.advanced)
var PropertyOptimizer = require('../../properties/optimizer');
var CleanUp = require('./clean-up');
-var Splitter = require('../../utils/splitter');
function AdvancedOptimizer(options, context) {
this.options = options;
this.propertyOptimizer = new PropertyOptimizer(this.options.compatibility, this.options.aggressiveMerging, context);
}
-function valueMapper(object) { return object.value; }
+function changeBodyOf(token, newBody) {
+ token.body = newBody.tokenized;
+ token.metadata.body = newBody.list.join(';');
+ token.metadata.bodiesList = newBody.list;
+}
+
+function changeSelectorOf(token, newSelectors) {
+ token.value = newSelectors.tokenized;
+ token.metadata.selector = newSelectors.list.join(',');
+ token.metadata.selectorsList = newSelectors.list;
+}
AdvancedOptimizer.prototype.isSpecial = function (selector) {
return this.options.compatibility.selectors.special.test(selector);
if (token.kind != 'selector')
continue;
- var id = token.body.map(valueMapper).join(';') + '@' + token.value.map(valueMapper).join(',');
+ var id = token.metadata.body + '@' + token.metadata.selector;
var alreadyMatched = matched[id];
if (alreadyMatched) {
if (token.kind != 'selector')
continue;
- // TODO: broken due to joining/splitting
- if (lastToken.kind == 'selector' && token.value.map(valueMapper).join(',') == lastToken.value.map(valueMapper).join(',')) {
+ if (lastToken.kind == 'selector' && token.metadata.selector == lastToken.metadata.selector) {
var joinAt = [lastToken.body.length];
- lastToken.body = this.propertyOptimizer.process(token.value, lastToken.body.concat(token.body), joinAt, true);
+ changeBodyOf(
+ lastToken,
+ 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.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));
+ } else if (lastToken.body && token.metadata.body == lastToken.metadata.body &&
+ !this.isSpecial(token.metadata.selector) && !this.isSpecial(lastToken.metadata.selector)) {
+ changeSelectorOf(
+ lastToken,
+ CleanUp.selectors(lastToken.value.concat(token.value))
+ );
forRemoval.push(i);
} else {
lastToken = token;
if (token.kind != 'selector')
continue;
- 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(',')];
+ var selectors = token.value.length > 1 && !this.isSpecial(token.metadata.selector) ?
+ [token.metadata.selector].concat(token.metadata.selectorsList) :
+ [token.metadata.selector];
for (var j = 0, m = selectors.length; j < m; j++) {
var selector = selectors[j];
else
moreThanOnce.push(selector);
- // TODO: broken due to joining/splitting
candidates[selector].push({
where: i,
- partial: selector != complexSelector.map(valueMapper).join(',')
+ partial: selector != token.metadata.selector,
+ list: token.metadata.selectorsList
});
}
}
},
callback: function (token, newBody, processedCount, tokenIdx) {
if (!data[processedCount - tokenIdx - 1].partial) {
- token.body = newBody;
+ changeBodyOf(token, newBody);
reduced = true;
}
}
var intoPosition = into[into.length - 1].where;
var intoToken = tokens[intoPosition];
- // TODO: broken due to joining/splitting
var selectors = this.isSpecial(complexSelector) ?
[complexSelector] :
- new Splitter(',').split(complexSelector);
+ into[0].list;
var reducedBodies = [];
for (var j = 0, m = selectors.length; j < m; j++) {
},
callback: function (token, newBody, processedCount, tokenIdx) {
if (tokenIdx === 0)
- reducedBodies.push(newBody.map(valueMapper).join(';'));
+ reducedBodies.push(newBody);
}
});
- if (reducedBodies[reducedBodies.length - 1] != reducedBodies[0])
+ if (reducedBodies[reducedBodies.length - 1].list.length != reducedBodies[0].list.length)
continue allSelectors;
}
- intoToken.body = reducedBodies[0].split(';').map(function (property) {
- return { value: property };
- });
+ intoToken.body = reducedBodies[0].tokenized;
reduced = true;
}
AdvancedOptimizer.prototype.reduceSelector = function (tokens, selector, data, options) {
var bodies = [];
+ var bodiesAsList = [];
var joinsAt = [];
- var splitBodies = [];
var processedTokens = [];
for (var j = data.length - 1, m = 0; j >= 0; j--) {
var where = data[j].where;
var token = tokens[where];
- var body = token.body;
- bodies = bodies.concat(body);
- splitBodies.push(body.map(valueMapper));
+ bodies = bodies.concat(token.body);
+ bodiesAsList.push(token.metadata.bodiesList);
processedTokens.push(where);
}
- for (j = 0, m = splitBodies.length; j < m; j++) {
- if (splitBodies[j].length > 0)
- joinsAt.push((joinsAt[j - 1] || 0) + splitBodies[j].length);
+ for (j = 0, m = bodiesAsList.length; j < m; j++) {
+ if (bodiesAsList[j].length > 0)
+ joinsAt.push((joinsAt[j - 1] || 0) + bodiesAsList[j].length);
}
var optimizedBody = this.propertyOptimizer.process(selector, bodies, joinsAt, false);
- var optimizedProperties = optimizedBody;
var processedCount = processedTokens.length;
- var propertyIdx = optimizedProperties.length - 1;
+ var propertyIdx = optimizedBody.tokenized.length - 1;
var tokenIdx = processedCount - 1;
while (tokenIdx >= 0) {
- if ((tokenIdx === 0 || (optimizedProperties[propertyIdx] && splitBodies[tokenIdx].indexOf(optimizedProperties[propertyIdx].value) > -1)) && propertyIdx > -1) {
+ if ((tokenIdx === 0 || (optimizedBody.tokenized[propertyIdx] && bodiesAsList[tokenIdx].indexOf(optimizedBody.tokenized[propertyIdx].value) > -1)) && propertyIdx > -1) {
propertyIdx--;
continue;
}
- var newBody = optimizedProperties.splice(propertyIdx + 1);
+ var newBody = {
+ list: optimizedBody.list.splice(propertyIdx + 1),
+ tokenized: optimizedBody.tokenized.splice(propertyIdx + 1)
+ };
options.callback(tokens[processedTokens[tokenIdx]], newBody, processedCount, tokenIdx);
tokenIdx--;
var token = tokens[i];
if (token.kind == 'selector') {
- token.body = propertyOptimizer.process(token.value, token.body, false, true);
+ changeBodyOf(
+ token,
+ propertyOptimizer.process(token.value, token.body, false, true)
+ );
} else if (token.kind == 'block') {
optimizeProperties(token.body, propertyOptimizer);
}
plain.push(reduced);
}
- return plain.sort().map(function (selector) {
- return { value: selector };
- });
+ var sorted = plain.sort();
+
+ return {
+ list: sorted,
+ tokenized: sorted.map(function (selector) { return { value: selector }; })
+ };
},
block: function (block) {
options.roundingPrecision;
options.precision.multiplier = Math.pow(10, options.precision.value);
options.precision.regexp = new RegExp('(\\d*\\.\\d{' + (options.precision.value + 1) + ',})px', 'g');
+
+ options.updateMetadata = this.options.advanced;
}
-function removeUnsupported(token, compatibility) {
- if (compatibility.selectors.ie7Hack)
- return;
+function removeUnsupported(selectors, options) {
+ if (options.compatibility.selectors.ie7Hack)
+ return false;
var supported = [];
- for (var i = 0, l = token.value.length; i < l; i++) {
- var selector = token.value[i];
+ var values = [];
+ for (var i = 0, l = selectors.length; i < l; i++) {
+ var selector = selectors[i];
- if (selector.value.indexOf('*+html ') === -1 && selector.value.indexOf('*:first-child+html ') === -1)
+ if (selector.value.indexOf('*+html ') === -1 && selector.value.indexOf('*:first-child+html ') === -1) {
supported.push(selector);
+ values.push(selector.value);
+ }
}
- token.value = supported;
+ return {
+ tokenized: supported,
+ list: values
+ };
}
var valueMinifiers = {
function reduce(body, options) {
var reduced = [];
+ var properties = [];
+ var newProperty;
for (var i = 0, l = body.length; i < l; i++) {
var token = body[i].value;
value = multipleZerosMinifier(property, value);
value = colorMininifier(property, value, options.compatibility);
- reduced.push({ value: property + ':' + value + (important ? '!important' : '') });
+ newProperty = property + ':' + value + (important ? '!important' : '');
+ reduced.push({ value: newProperty });
+ properties.push(newProperty);
}
- return reduced;
+ return {
+ tokenized: reduced,
+ list: properties
+ };
}
SimpleOptimizer.prototype.optimize = function(tokens) {
var self = this;
var hasCharset = false;
+ var options = this.options;
function _optimize(tokens) {
for (var i = 0, l = tokens.length; i < l; i++) {
break;
if (token.kind == 'selector') {
- token.value = CleanUp.selectors(token.value);
+ var newSelectors = removeUnsupported(CleanUp.selectors(token.value).tokenized, options);
+ if (newSelectors)
+ token.value = newSelectors.tokenized;
- removeUnsupported(token, self.options.compatibility);
if (token.value.length === 0) {
tokens.splice(i, 1);
i--;
continue;
}
-
- token.body = reduce(token.body, self.options);
+ var newBody = reduce(token.body, self.options);
+ token.body = newBody.tokenized;
+
+ if (options.updateMetadata) {
+ token.metadata.body = newBody.list.join(';');
+ token.metadata.bodiesList = newBody.list;
+ if (newSelectors) {
+ token.metadata.selector = newSelectors.list.join(',');
+ token.metadata.selectorsList = newSelectors.list;
+ }
+ }
} else if (token.kind == 'block') {
token.value = CleanUp.block(token.value);
if (token.isFlatBlock)
- token.body = reduce(token.body, self.options);
+ token.body = reduce(token.body, self.options).tokenized;
else
_optimize(token.body);
} else if (token.kind == 'text') {
var MULTI_WHITESPACE = /\s{2,}/g;
var WHITESPACE_COMMA = / ?, ?/g;
-function Tokenizer(minifyContext) {
+function Tokenizer(minifyContext, addMetadata) {
this.minifyContext = minifyContext;
+ this.addMetadata = addMetadata;
}
Tokenizer.prototype.toTokens = function (data) {
mode: 'top',
chunker: chunker,
chunk: chunker.next(),
- outer: this.minifyContext
+ outer: this.minifyContext,
+ addMetadata: this.addMetadata
};
return tokenize(context);
function valueMapper(property) { return { value: property }; }
function extractProperties(string) {
- var properties = [];
+ var tokenized = [];
+ var list = [];
var buffer = [];
+ var property;
var isWhitespace;
var wasWhitespace;
var isSpecial;
if (current === ';') {
if (wasWhitespace && buffer[buffer.length - 1] === ' ')
buffer.pop();
- if (buffer.length > 0)
- properties.push({ value: buffer.join('') });
+ if (buffer.length > 0) {
+ property = buffer.join('');
+ tokenized.push({ value: property });
+ list.push(property);
+ }
buffer = [];
} else {
isWhitespace = current === ' ' || current === '\t' || current === '\n';
if (wasWhitespace && buffer[buffer.length - 1] === ' ')
buffer.pop();
- if (buffer.length > 0)
- properties.push({ value: buffer.join('') });
+ if (buffer.length > 0) {
+ property = buffer.join('');
+ tokenized.push({ value: property });
+ list.push(property);
+ }
- return properties;
+ return {
+ list: list,
+ tokenized: tokenized
+ };
}
function extractSelectors(string) {
.replace(WHITESPACE_COMMA, ',')
.trim();
- return new Splitter(',').split(extracted).map(valueMapper);
+ var selectors = new Splitter(',').split(extracted);
+ return {
+ list: selectors,
+ tokenized: selectors.map(valueMapper)
+ };
}
function extractBlock(string) {
var specialBody = tokenize(context);
if (typeof specialBody == 'string')
- specialBody = extractProperties(specialBody);
+ specialBody = extractProperties(specialBody).tokenized;
context.mode = oldMode;
context.cursor = nextEnd + 2;
} else if (what == 'bodyStart') {
- var selector = extractSelectors(chunk.substring(context.cursor, nextSpecial));
+ var selectorData = extractSelectors(chunk.substring(context.cursor, nextSpecial));
oldMode = context.mode;
context.cursor = nextSpecial + 1;
context.mode = 'body';
- var body = extractProperties(tokenize(context));
+ var bodyData = extractProperties(tokenize(context));
context.mode = oldMode;
- tokenized.push({ kind: 'selector', value: selector, body: body });
+ var newToken = {
+ kind: 'selector',
+ value: selectorData.tokenized,
+ body: bodyData.tokenized
+ };
+ if (context.addMetadata) {
+ newToken.metadata = {
+ body: bodyData.list.join(','),
+ bodiesList: bodyData.list,
+ selector: selectorData.list.join(','),
+ selectorsList: selectorData.list
+ };
+ }
+ tokenized.push(newToken);
} else if (what == 'bodyEnd') {
// extra closing brace at the top level can be safely ignored
if (context.mode == 'top') {
var assert = require('assert');
var Tokenizer = require('../../lib/selectors/tokenizer');
-function tokenizerContext(name, specs) {
+function tokenizerContext(name, specs, addMetadata) {
var ctx = {};
function tokenized(target) {
return function (source) {
- var tokenized = new Tokenizer({}).toTokens(source);
+ var tokenized = new Tokenizer({}, addMetadata).toTokens(source);
assert.deepEqual(target, tokenized);
};
}
]
})
)
+ .addBatch(
+ tokenizerContext('metadata', {
+ 'no content': [
+ '',
+ []
+ ],
+ 'an escaped content': [
+ '__ESCAPED_COMMENT_CLEAN_CSS0__',
+ [{ kind: 'text', value: '__ESCAPED_COMMENT_CLEAN_CSS0__' }]
+ ],
+ 'an empty selector': [
+ 'a{}',
+ [{
+ kind: 'selector',
+ value: [{ value: 'a' }],
+ body: [],
+ metadata: {
+ body: '',
+ bodiesList: [],
+ selector: 'a',
+ selectorsList: ['a']
+ }
+ }]
+ ],
+ 'a double selector': [
+ 'a,\n\ndiv.class > p {color:red}',
+ [{
+ kind: 'selector',
+ value: [{ value: 'a' }, { value: 'div.class > p' }],
+ body: [{ value: 'color:red' }],
+ metadata: {
+ body: 'color:red',
+ bodiesList: ['color:red'],
+ selector: 'a,div.class > p',
+ selectorsList: ['a', 'div.class > p']
+ }
+ }],
+ ],
+ 'two selectors': [
+ 'a{color:red}div{color:blue}',
+ [
+ {
+ kind: 'selector',
+ value: [{ value: 'a' }],
+ body: [{ value: 'color:red' }],
+ metadata: {
+ body: 'color:red',
+ bodiesList: ['color:red'],
+ selector: 'a',
+ selectorsList: ['a']
+ }
+ },
+ {
+ kind: 'selector',
+ value: [{ value: 'div' }],
+ body: [{ value: 'color:blue' }],
+ metadata: {
+ body: 'color:blue',
+ bodiesList: ['color:blue'],
+ selector: 'div',
+ selectorsList: ['div']
+ }
+ }
+ ]
+ ]
+ }, true)
+ )
.export(module);