From: Jakub Pawlowicz Date: Mon, 24 Aug 2015 04:37:23 +0000 (+0100) Subject: Adds support for Polymer mixins. X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=21a9399ca86a5cac4eb6c1e8ce083e27aab0b107;p=clean-css.git Adds support for Polymer mixins. We lacked support of property-level blocks (just simple values) so this commit adds this with a special handling of properties in tokenizer, simple & advanced optimization, and stringifier. See: https://www.polymer-project.org/1.0/docs/devguide/styling.html#custom-css-mixins --- diff --git a/History.md b/History.md index 2d6946da..c4db416a 100644 --- a/History.md +++ b/History.md @@ -4,6 +4,7 @@ * Adds unit compatibility switches to disable length optimizations. * Adds inferring proxy settings from HTTP_PROXY environment variable. * Adds support for Polymer / Web Components special selectors. +* Adds support for Polymer mixins. * Unifies wrappers for simple & advanced optimizations. * Fixed issue [#596](https://github.com/jakubpawlowicz/clean-css/issues/596) - support for !ie IE<8 hack. * Fixed issue [#599](https://github.com/jakubpawlowicz/clean-css/issues/599) - support for inlined source maps. diff --git a/lib/properties/optimizer.js b/lib/properties/optimizer.js index 54c7c12f..702a63d2 100644 --- a/lib/properties/optimizer.js +++ b/lib/properties/optimizer.js @@ -188,6 +188,12 @@ function optimize(selector, properties, mergeAdjacent, withCompacting, options, populateComponents(_properties, validator); _optimize(_properties, mergeAdjacent, options.aggressiveMerging, validator); + for (var i = 0, l = _properties.length; i < l; i++) { + var _property = _properties[i]; + if (_property.variable && _property.block) + optimize(selector, _property.value[0], mergeAdjacent, withCompacting, options, validator); + } + if (withCompacting && options.shorthandCompacting) { compactOverrides(_properties, options.compatibility, validator); compactShorthands(_properties, options.sourceMap, validator); diff --git a/lib/properties/override-compactor.js b/lib/properties/override-compactor.js index a8a1eb99..c25ce798 100644 --- a/lib/properties/override-compactor.js +++ b/lib/properties/override-compactor.js @@ -226,6 +226,9 @@ function compactOverrides(properties, compatibility, validator) { if (!isCompactable(right)) continue; + if (right.variable) + continue; + mayOverride = compactable[right.name].canOverride || canOverride.sameValue; for (j = i - 1; j >= 0; j--) { @@ -234,6 +237,9 @@ function compactOverrides(properties, compatibility, validator) { if (!isCompactable(left)) continue; + if (left.variable) + continue; + if (left.unused || right.unused) continue; diff --git a/lib/properties/shorthand-compactor.js b/lib/properties/shorthand-compactor.js index e378ba37..1cd48f6e 100644 --- a/lib/properties/shorthand-compactor.js +++ b/lib/properties/shorthand-compactor.js @@ -112,6 +112,9 @@ function compactShortands(properties, sourceMaps, validator) { if (property.hack) continue; + if (property.variable) + continue; + var descriptor = compactable[property.name]; if (!descriptor || !descriptor.componentOf) continue; diff --git a/lib/properties/wrap-for-optimizing.js b/lib/properties/wrap-for-optimizing.js index cf493e6d..4de3c644 100644 --- a/lib/properties/wrap-for-optimizing.js +++ b/lib/properties/wrap-for-optimizing.js @@ -87,7 +87,10 @@ function wrapSingle(property) { else if (_hackType == 'backslash' || _hackType == 'bang') stripSuffixHack(property, _hackType); + var isVariable = property[0][0].indexOf('--') === 0; + return { + block: isVariable && property[1] && Array.isArray(property[1][0][0]), components: [], dirty: false, hack: _hackType, @@ -97,7 +100,8 @@ function wrapSingle(property) { position: 0, shorthand: false, unused: property.length < 2, - value: property.slice(1) + value: property.slice(1), + variable: isVariable }; } diff --git a/lib/selectors/extractor.js b/lib/selectors/extractor.js index df62e0ea..497198b2 100644 --- a/lib/selectors/extractor.js +++ b/lib/selectors/extractor.js @@ -25,6 +25,9 @@ function extract(token) { if (name.length === 0) continue; + if (name.indexOf('--') === 0) + continue; + var value = stringifyValue(token[2], i); properties.push([ diff --git a/lib/selectors/simple.js b/lib/selectors/simple.js index da586ab3..3e27e9d2 100644 --- a/lib/selectors/simple.js +++ b/lib/selectors/simple.js @@ -311,6 +311,12 @@ function optimizeBody(properties, options) { if (property.unused) continue; + if (property.variable) { + if (property.block) + optimizeBody(property.value[0], options); + continue; + } + for (var j = 0, m = property.value.length; j < m; j++) { value = property.value[j][0]; diff --git a/lib/stringifier/helpers.js b/lib/stringifier/helpers.js index 14116191..32b331ef 100644 --- a/lib/stringifier/helpers.js +++ b/lib/stringifier/helpers.js @@ -95,6 +95,14 @@ function propertyAtRule(value, isLast, context) { function value(tokens, position, isLast, context) { var store = context.store; var token = tokens[position]; + var isVariableDeclaration = token[0][0].indexOf('--') === 0; + + if (isVariableDeclaration && Array.isArray(token[1][0][0])) { + store('{', context); + body(token[1], context); + store('};', context); + return; + } for (var j = 1, m = token.length; j < m; j++) { store(token[j], context); diff --git a/lib/tokenizer/extract-properties.js b/lib/tokenizer/extract-properties.js index 2eca69c9..1805a843 100644 --- a/lib/tokenizer/extract-properties.js +++ b/lib/tokenizer/extract-properties.js @@ -57,7 +57,7 @@ function extractProperties(string, selectors, context) { if (string.indexOf('ESCAPED_URL_CLEAN_CSS') > -1) string = string.replace(/(ESCAPED_URL_CLEAN_CSS[^_]+?__)/g, context.sourceMap ? '$1 __ESCAPED_COMMENT_CLEAN_CSS(0,-1)__ ' : '$1 '); - var candidates = string.split(';'); + var candidates = split(string, ';', false, '{', '}'); for (var i = 0, l = candidates.length; i < l; i++) { var candidate = candidates[i]; @@ -77,7 +77,7 @@ function extractProperties(string, selectors, context) { continue; } - if (candidate.indexOf('{') > 0) { + if (candidate.indexOf('{') > 0 && candidate.indexOf('{') < firstColonAt) { context.track(candidate); continue; } @@ -98,6 +98,22 @@ function extractProperties(string, selectors, context) { trackComments(innerComments, list, context); + var firstBraceAt = candidate.indexOf('{'); + var isVariable = name.trim().indexOf('--') === 0; + if (isVariable && firstBraceAt > 0) { + var blockPrefix = candidate.substring(firstColonAt + 1, firstBraceAt + 1); + var blockSuffix = candidate.substring(candidate.indexOf('}')); + var blockContent = candidate.substring(firstBraceAt + 1, candidate.length - blockSuffix.length); + + context.track(blockPrefix); + body.push(extractProperties(blockContent, selectors, context)); + list.push(body); + context.track(blockSuffix); + context.track(i < l - 1 ? ';' : ''); + + continue; + } + var values = split(candidate.substring(firstColonAt + 1), valueSeparator, true); if (values.length == 1 && values[0] === '') { diff --git a/lib/tokenizer/tokenize.js b/lib/tokenizer/tokenize.js index 3061dea7..449a3cb9 100644 --- a/lib/tokenizer/tokenize.js +++ b/lib/tokenizer/tokenize.js @@ -1,6 +1,7 @@ var extractProperties = require('./extract-properties'); var extractSelectors = require('./extract-selectors'); var track = require('../source-maps/track'); +var split = require('../utils/split'); var path = require('path'); @@ -65,10 +66,14 @@ function whatsNext(context) { } if (mode == 'body') { - closest = chunk.indexOf('}', context.cursor); - return closest > -1 ? - [closest, 'bodyEnd'] : - null; + if (chunk[context.cursor] == '}') + return [context.cursor, 'bodyEnd']; + + if (chunk.indexOf('}', context.cursor) == -1) + return null; + + closest = context.cursor + split(chunk.substring(context.cursor - 1), '}', true, '{', '}')[0].length - 2; + return [closest, 'bodyEnd']; } var nextSpecial = chunk.indexOf('@', context.cursor); diff --git a/test/integration-test.js b/test/integration-test.js index b6ec0b5b..598c921e 100644 --- a/test/integration-test.js +++ b/test/integration-test.js @@ -1635,7 +1635,7 @@ vows.describe('integration tests') ) .addBatch( optimizerContext('IE 7 hacks', { - 'keeps !ie hack 123': [ + 'keeps !ie hack': [ 'a{list-style-type:none;list-style-type:decimal !ie}', 'a{list-style-type:none;list-style-type:decimal !ie}' ] @@ -2571,6 +2571,22 @@ vows.describe('integration tests') 'all values': [ 'a{--width:1px;--style:solid;--color:#000}.one{border:var(--width)var(--style)var(--color)}', 'a{--width:1px;--style:solid;--color:#000}.one{border:var(--width)var(--style)var(--color)}' + ], + 'Polymer mixins - simple optimizations': [ + 'a{ display:block; --my-toolbar: { color:#f00; width:96px }; color:blue}', + 'a{display:block;--my-toolbar:{color:red;width:1in};color:#00f}' + ], + 'Polymer mixins - override optimizations': [ + 'a{--my-toolbar: { margin:15px!important; margin:10px};}', + 'a{--my-toolbar:{margin:15px!important};}' + ], + 'Polymer mixins - shorthand optimizations': [ + 'a{--my-toolbar: { margin:10px; margin-top:10px };}', + 'a{--my-toolbar:{margin:10px};}' + ], + 'Polymer mixins - not fitting into a single chunk of 128 bytes': [ + ':host{--live-head-theme: { line-height: 40px !important; vertical-align: middle; background: transparent; height: 40px; z-index: 999; }; }', + ':host{--live-head-theme:{line-height:40px!important;vertical-align:middle;background:0 0;height:40px;z-index:999};}' ] }) ) diff --git a/test/properties/wrap-for-optimizing-test.js b/test/properties/wrap-for-optimizing-test.js index 67a05c92..8c699d28 100644 --- a/test/properties/wrap-for-optimizing-test.js +++ b/test/properties/wrap-for-optimizing-test.js @@ -18,6 +18,9 @@ vows.describe(wrapForOptimizing) 'has value': function (wrapped) { assert.deepEqual(wrapped[0].value, [['0'], ['0']]); }, + 'is not a block': function (wrapped) { + assert.isFalse(wrapped[0].block); + }, 'has no components': function (wrapped) { assert.lengthOf(wrapped[0].components, 0); }, @@ -76,6 +79,46 @@ vows.describe(wrapForOptimizing) assert.isTrue(wrapped[0].multiplex); } }, + 'variable': { + 'topic': function () { + return wrapForOptimizing([[['--color'], ['red']]]); + }, + 'has one wrap': function (wrapped) { + assert.lengthOf(wrapped, 1); + }, + 'has name': function (wrapped) { + assert.deepEqual(wrapped[0].name, '--color'); + }, + 'has value': function (wrapped) { + assert.deepEqual(wrapped[0].value, [['red']]); + }, + 'is not a block': function (wrapped) { + assert.isFalse(wrapped[0].block); + }, + 'is variable': function (wrapped) { + assert.isTrue(wrapped[0].variable); + } + }, + 'variable block': { + 'topic': function () { + return wrapForOptimizing([[['--color'], [ [['color'], ['red']], [['text-color'], ['red']] ]]]); + }, + 'has one wrap': function (wrapped) { + assert.lengthOf(wrapped, 1); + }, + 'has name': function (wrapped) { + assert.deepEqual(wrapped[0].name, '--color'); + }, + 'has value': function (wrapped) { + assert.deepEqual(wrapped[0].value, [[ [['color'], ['red']], [['text-color'], ['red']] ]]); + }, + 'is not a block': function (wrapped) { + assert.isTrue(wrapped[0].block); + }, + 'is variable': function (wrapped) { + assert.isTrue(wrapped[0].variable); + } + }, 'without value': { 'topic': function () { return wrapForOptimizing([[['margin']]]); diff --git a/test/selectors/extractor-test.js b/test/selectors/extractor-test.js index 09e74593..6d2fd88f 100644 --- a/test/selectors/extractor-test.js +++ b/test/selectors/extractor-test.js @@ -39,6 +39,18 @@ vows.describe(extractor) assert.deepEqual(tokens, [['color', 'red', 'color', [['color'], ['red']], 'color:red', [['#one span']], true]]); } }, + 'one property - variable': { + 'topic': extractor(buildToken('#one span{--color:red}')), + 'has no properties': function (tokens) { + assert.deepEqual(tokens, []); + } + }, + 'one property - block variable': { + 'topic': extractor(buildToken('#one span{--color:{color:red;display:block};}')), + 'has no properties': function (tokens) { + assert.deepEqual(tokens, []); + } + }, 'one property - complex selector': { 'topic': extractor(buildToken('.one{color:red}')), 'has no properties': function (tokens) { diff --git a/test/tokenizer/tokenizer-source-maps-test.js b/test/tokenizer/tokenizer-source-maps-test.js index 97c23048..0fd62f34 100644 --- a/test/tokenizer/tokenizer-source-maps-test.js +++ b/test/tokenizer/tokenizer-source-maps-test.js @@ -358,6 +358,19 @@ vows.describe('source-maps/analyzer') ] ] ] + ], + 'variable blocks': [ + '\ndiv {\n--test1:{\n color:red}; --test2:{ color: blue };}', + [ + [ + 'selector', + [['div', [[2, 0, undefined]]]], + [ + [['--test1', [[3, 0, undefined]]], [[['color', [[4, 1, undefined]]], ['red', [[4, 7, undefined]] ]]]], + [['--test2', [[4, 13, undefined]]], [[['color', [[4, 23, undefined]]], ['blue', [[4, 30, undefined]] ]]]] + ] + ] + ] ] }) ) diff --git a/test/tokenizer/tokenizer-test.js b/test/tokenizer/tokenizer-test.js index ca44bc03..a43e6b91 100644 --- a/test/tokenizer/tokenizer-test.js +++ b/test/tokenizer/tokenizer-test.js @@ -289,6 +289,43 @@ vows.describe(tokenize) ] }) ) + .addBatch( + tokenizerContext('Polymer mixins', { + 'flat value': [ + 'a{--my-toolbar-color:red}', + [ + ['selector', [['a']], [[['--my-toolbar-color'], ['red']]]] + ] + ], + 'block value': [ + 'a{--my-toolbar:{color:red;width:100%}}', + [ + [ + 'selector', + [['a']], + [[['--my-toolbar'], [ + [['color'], ['red']], + [['width'], ['100%']] + ]]] + ] + ] + ], + 'mixed block value': [ + 'a{display:block;--my-toolbar:{color:red;width:100%};color:blue}', + [ + [ + 'selector', + [['a']], + [ + [['display'], ['block']], + [['--my-toolbar'], [[['color'], ['red']], [['width'], ['100%']]]], + [['color'], ['blue']] + ] + ] + ] + ] + }) + ) .addBatch( tokenizerContext('multiple values', { 'comma - no spaces': [