From: Jakub Pawlowicz Date: Thu, 23 Oct 2014 21:46:21 +0000 (+0100) Subject: Reworks simple & advanced optimisations to use metadata. X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=c8653779635e346cc78336d7f4ee8b28ab8d359a;p=clean-css.git Reworks simple & advanced optimisations to use metadata. * We can avoid merging, splitting, and mapping by using token metadata directly. * Unfortunately it means metadata has to be updated as we go. --- diff --git a/lib/properties/optimizer.js b/lib/properties/optimizer.js index 7f617619..c44eb32f 100644 --- a/lib/properties/optimizer.js +++ b/lib/properties/optimizer.js @@ -237,19 +237,23 @@ module.exports = function Optimizer(compatibility, aggressiveMerging, context) { }; 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 }; }; @@ -272,8 +276,8 @@ module.exports = function Optimizer(compatibility, aggressiveMerging, context) { var rebuilt = rebuild(optimized); return compactProperties && rebuilt.compactFurther ? - compact(rebuilt.value) : - rebuilt.value; + compact(rebuilt.tokenized) : + rebuilt; } }; }; diff --git a/lib/properties/token.js b/lib/properties/token.js index bb4b8c71..333d43fc 100644 --- a/lib/properties/token.js +++ b/lib/properties/token.js @@ -110,7 +110,8 @@ module.exports = (function() { 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 @@ -124,10 +125,15 @@ module.exports = (function() { 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 diff --git a/lib/selectors/optimizer.js b/lib/selectors/optimizer.js index a65b3bc2..0454bd8f 100644 --- a/lib/selectors/optimizer.js +++ b/lib/selectors/optimizer.js @@ -32,7 +32,7 @@ function rebuild(tokens, keepBreaks, isFlatBlock) { 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; @@ -53,7 +53,7 @@ function rebuild(tokens, keepBreaks, isFlatBlock) { } 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) diff --git a/lib/selectors/optimizers/advanced.js b/lib/selectors/optimizers/advanced.js index 142b1ed2..fd3f9984 100644 --- a/lib/selectors/optimizers/advanced.js +++ b/lib/selectors/optimizers/advanced.js @@ -1,6 +1,5 @@ var PropertyOptimizer = require('../../properties/optimizer'); var CleanUp = require('./clean-up'); -var Splitter = require('../../utils/splitter'); function AdvancedOptimizer(options, context) { this.options = options; @@ -8,7 +7,17 @@ function AdvancedOptimizer(options, context) { 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); @@ -23,7 +32,7 @@ AdvancedOptimizer.prototype.removeDuplicates = function (tokens) { 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) { @@ -55,14 +64,19 @@ AdvancedOptimizer.prototype.mergeAdjacent = function (tokens) { 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; @@ -86,10 +100,9 @@ AdvancedOptimizer.prototype.reduceNonAdjacent = function (tokens) { 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]; @@ -99,10 +112,10 @@ AdvancedOptimizer.prototype.reduceNonAdjacent = function (tokens) { 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 }); } } @@ -130,7 +143,7 @@ AdvancedOptimizer.prototype.reduceSimpleNonAdjacentCases = function (tokens, mat }, callback: function (token, newBody, processedCount, tokenIdx) { if (!data[processedCount - tokenIdx - 1].partial) { - token.body = newBody; + changeBodyOf(token, newBody); reduced = true; } } @@ -152,10 +165,9 @@ AdvancedOptimizer.prototype.reduceComplexNonAdjacentCases = function (tokens, po 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++) { @@ -172,17 +184,15 @@ AdvancedOptimizer.prototype.reduceComplexNonAdjacentCases = function (tokens, po }, 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; } @@ -191,8 +201,8 @@ AdvancedOptimizer.prototype.reduceComplexNonAdjacentCases = function (tokens, po 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--) { @@ -201,32 +211,33 @@ AdvancedOptimizer.prototype.reduceSelector = function (tokens, selector, data, o 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--; @@ -238,7 +249,10 @@ function optimizeProperties(tokens, propertyOptimizer) { 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); } diff --git a/lib/selectors/optimizers/clean-up.js b/lib/selectors/optimizers/clean-up.js index dbe5fb02..55ef9e8b 100644 --- a/lib/selectors/optimizers/clean-up.js +++ b/lib/selectors/optimizers/clean-up.js @@ -23,9 +23,12 @@ var CleanUp = { 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) { diff --git a/lib/selectors/optimizers/simple.js b/lib/selectors/optimizers/simple.js index 28e33202..a4a66104 100644 --- a/lib/selectors/optimizers/simple.js +++ b/lib/selectors/optimizers/simple.js @@ -23,21 +23,29 @@ function SimpleOptimizer(options) { 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 = { @@ -171,6 +179,8 @@ function colorMininifier(property, value, compatibility) { 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; @@ -199,15 +209,21 @@ function reduce(body, options) { 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++) { @@ -217,20 +233,30 @@ SimpleOptimizer.prototype.optimize = function(tokens) { 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') { diff --git a/lib/selectors/tokenizer.js b/lib/selectors/tokenizer.js index 0bd21d3d..6eeb75ab 100644 --- a/lib/selectors/tokenizer.js +++ b/lib/selectors/tokenizer.js @@ -6,8 +6,9 @@ var WHITESPACE = /\s/g; 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) { @@ -22,7 +23,8 @@ Tokenizer.prototype.toTokens = function (data) { mode: 'top', chunker: chunker, chunk: chunker.next(), - outer: this.minifyContext + outer: this.minifyContext, + addMetadata: this.addMetadata }; return tokenize(context); @@ -31,8 +33,10 @@ Tokenizer.prototype.toTokens = function (data) { 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; @@ -46,8 +50,11 @@ function extractProperties(string) { 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'; @@ -73,10 +80,16 @@ function extractProperties(string) { 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) { @@ -86,7 +99,11 @@ 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) { @@ -192,7 +209,7 @@ function tokenize(context) { var specialBody = tokenize(context); if (typeof specialBody == 'string') - specialBody = extractProperties(specialBody); + specialBody = extractProperties(specialBody).tokenized; context.mode = oldMode; @@ -205,16 +222,29 @@ function tokenize(context) { 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') { diff --git a/test/selectors/tokenizer-test.js b/test/selectors/tokenizer-test.js index b5e1130f..fc3e57ff 100644 --- a/test/selectors/tokenizer-test.js +++ b/test/selectors/tokenizer-test.js @@ -2,12 +2,12 @@ var vows = require('vows'); 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); }; } @@ -204,4 +204,71 @@ vows.describe(Tokenizer) ] }) ) + .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);