* Fixed issue [#893](https://github.com/jakubpawlowicz/clean-css/issues/893) - `inline: false` as alias to `inline: 'none'`.
* Fixed issue [#890](https://github.com/jakubpawlowicz/clean-css/issues/890) - adds toggle to disable empty tokens removal.
* Fixed issue [#886](https://github.com/jakubpawlowicz/clean-css/issues/886) - better multi pseudo class / element merging.
+* Fixed issue [#862](https://github.com/jakubpawlowicz/clean-css/issues/862) - allows removing unused at rules.
* Fixed issue [#860](https://github.com/jakubpawlowicz/clean-css/issues/860) - adds `animation` property optimizer.
* Fixed issue [#755](https://github.com/jakubpawlowicz/clean-css/issues/755) - adds custom handling of remote requests.
* Fixed issue [#254](https://github.com/jakubpawlowicz/clean-css/issues/254) - adds `font` property optimizer.
* removal of `optimizeFont` flag in level 1 optimizations due to new `font` shorthand optimizer;
* `skipProperties` flag in level 2 optimizations controlling which properties won't be optimized;
* new `animation` shorthand and `animation-*` longhand optimizers;
+* `removeUnusedAtRules` level 2 optimization controlling removal of unused `@counter-style`, `@font-face`, `@keyframes`, and `@namespace` at rules;
## Constructor options
removeDuplicateFontRules: true, // controls duplicate `@font-face` removing; defaults to true
removeDuplicateMediaBlocks: true, // controls duplicate `@media` removing; defaults to true
removeDuplicateRules: true, // controls duplicate rules removing; defaults to true
- restructureRules: false, // controls rule restructuring; defaults to false`
+ removeUnusedAtRules: false, // controls unused at rule removing; defaults to false (available since 4.1.0-pre)
+ restructureRules: false, // controls rule restructuring; defaults to false
skipProperties: [] // controls which properties won't be optimized, defaults to `[]` which means all will be optimized (since 4.1.0-pre)
}
}
var removeDuplicateFontAtRules = require('./remove-duplicate-font-at-rules');
var removeDuplicateMediaQueries = require('./remove-duplicate-media-queries');
var removeDuplicates = require('./remove-duplicates');
+var removeUnusedAtRules = require('./remove-unused-at-rules');
var restructure = require('./restructure');
var optimizeProperties = require('./properties/optimize');
removeEmpty(token[2]);
isEmpty = token[2].length === 0;
break;
+ case Token.AT_RULE:
+ isEmpty = token[1].length === 0;
+ break;
case Token.AT_RULE_BLOCK:
isEmpty = token[2].length === 0;
}
removeDuplicateMediaQueries(tokens, context);
}
+ if (levelOptions.removeUnusedAtRules) {
+ removeUnusedAtRules(tokens, context);
+ }
+
if (levelOptions.mergeMedia) {
reduced = mergeMediaQueries(tokens, context);
for (i = reduced.length - 1; i >= 0; i--) {
--- /dev/null
+var populateComponents = require('./properties/populate-components');
+
+var wrapForOptimizing = require('../wrap-for-optimizing').single;
+
+var Token = require('../../tokenizer/token');
+
+var animationNameRegex = /^(\-moz\-|\-o\-|\-webkit\-)?animation-name$/;
+var animationRegex = /^(\-moz\-|\-o\-|\-webkit\-)?animation$/;
+var keyframeRegex = /^@(\-moz\-|\-o\-|\-webkit\-)?keyframes /;
+
+function removeUnusedAtRules(tokens, context) {
+ removeUnusedAtRule(tokens, matchCounterStyle, markCounterStylesAsUsed, context);
+ removeUnusedAtRule(tokens, matchFontFace, markFontFacesAsUsed, context);
+ removeUnusedAtRule(tokens, matchKeyframe, markKeyframesAsUsed, context);
+ removeUnusedAtRule(tokens, matchNamespace, markNamespacesAsUsed, context);
+}
+
+function removeUnusedAtRule(tokens, matchCallback, markCallback, context) {
+ var atRules = {};
+ var atRule;
+ var token;
+ var zeroAt;
+ var i, l;
+
+ for (i = 0, l = tokens.length; i < l; i++) {
+ matchCallback(tokens[i], atRules);
+ }
+
+ if (Object.keys(atRules).length === 0) {
+ return;
+ }
+
+ markUsedAtRules(tokens, markCallback, atRules, context);
+
+ for (atRule in atRules) {
+ token = atRules[atRule];
+ zeroAt = token[0] == Token.AT_RULE ? 1 : 2;
+ token[zeroAt] = [];
+ }
+}
+
+function markUsedAtRules(tokens, markCallback, atRules, context) {
+ var boundMarkCallback = markCallback(atRules);
+ var i, l;
+
+ for (i = 0, l = tokens.length; i < l; i++) {
+ switch (tokens[i][0]) {
+ case Token.RULE:
+ boundMarkCallback(tokens[i], context);
+ break;
+ case Token.NESTED_BLOCK:
+ markUsedAtRules(tokens[i][2], markCallback, atRules, context);
+ }
+ }
+}
+
+function matchCounterStyle(token, atRules) {
+ var match;
+
+ if (token[0] == Token.AT_RULE_BLOCK && token[1][0][1].indexOf('@counter-style') === 0) {
+ match = token[1][0][1].split(' ')[1];
+ atRules[match] = token;
+ }
+}
+
+function markCounterStylesAsUsed(atRules) {
+ return function (token, context) {
+ var property;
+ var wrappedProperty;
+ var i, l;
+
+ for (i = 0, l = token[2].length; i < l; i++) {
+ property = token[2][i];
+
+ if (property[1][1] == 'list-style') {
+ wrappedProperty = wrapForOptimizing(property);
+ populateComponents([wrappedProperty], context.validator, context.warnings);
+
+ if (wrappedProperty.components[0].value[0][1] in atRules) {
+ delete atRules[property[2][1]];
+ }
+ }
+
+ if (property[1][1] == 'list-style-type' && property[2][1] in atRules) {
+ delete atRules[property[2][1]];
+ }
+ }
+ };
+}
+
+function matchFontFace(token, atRules) {
+ var property;
+ var match;
+ var i, l;
+
+ if (token[0] == Token.AT_RULE_BLOCK && token[1][0][1] == '@font-face') {
+ for (i = 0, l = token[2].length; i < l; i++) {
+ property = token[2][i];
+
+ if (property[1][1] == 'font-family') {
+ match = property[2][1].toLowerCase();
+ atRules[match] = token;
+ break;
+ }
+ }
+ }
+}
+
+function markFontFacesAsUsed(atRules) {
+ return function (token, context) {
+ var property;
+ var wrappedProperty;
+ var component;
+ var normalizedMatch;
+ var i, l;
+ var j, m;
+
+ for (i = 0, l = token[2].length; i < l; i++) {
+ property = token[2][i];
+
+ if (property[1][1] == 'font') {
+ wrappedProperty = wrapForOptimizing(property);
+ populateComponents([wrappedProperty], context.validator, context.warnings);
+ component = wrappedProperty.components[6];
+
+ for (j = 0, m = component.value.length; j < m; j++) {
+ normalizedMatch = component.value[j][1].toLowerCase();
+
+ if (normalizedMatch in atRules) {
+ delete atRules[normalizedMatch];
+ }
+ }
+ }
+
+ if (property[1][1] == 'font-family') {
+ for (j = 2, m = property.length; j < m; j++) {
+ normalizedMatch = property[j][1].toLowerCase();
+
+ if (normalizedMatch in atRules) {
+ delete atRules[normalizedMatch];
+ }
+ }
+ }
+ }
+ };
+}
+
+function matchKeyframe(token, atRules) {
+ var match;
+
+ if (token[0] == Token.NESTED_BLOCK && keyframeRegex.test(token[1][0][1])) {
+ match = token[1][0][1].split(' ')[1];
+ atRules[match] = token;
+ }
+}
+
+function markKeyframesAsUsed(atRules) {
+ return function (token, context) {
+ var property;
+ var wrappedProperty;
+ var component;
+ var i, l;
+ var j, m;
+
+ for (i = 0, l = token[2].length; i < l; i++) {
+ property = token[2][i];
+
+ if (animationRegex.test(property[1][1])) {
+ wrappedProperty = wrapForOptimizing(property);
+ populateComponents([wrappedProperty], context.validator, context.warnings);
+ component = wrappedProperty.components[7];
+
+ for (j = 0, m = component.value.length; j < m; j++) {
+ if (component.value[j][1] in atRules) {
+ delete atRules[component.value[j][1]];
+ }
+ }
+ }
+
+ if (animationNameRegex.test(property[1][1])) {
+ for (j = 2, m = property.length; j < m; j++) {
+ if (property[j][1] in atRules) {
+ delete atRules[property[j][1]];
+ }
+ }
+ }
+ }
+ };
+}
+
+function matchNamespace(token, atRules) {
+ var match;
+
+ if (token[0] == Token.AT_RULE && token[1].indexOf('@namespace') === 0) {
+ match = token[1].split(' ')[1];
+ atRules[match] = token;
+ }
+}
+
+function markNamespacesAsUsed(atRules) {
+ var namespaceRegex = new RegExp(Object.keys(atRules).join('\\\||') + '\\\|', 'g');
+
+ return function (token) {
+ var match;
+ var scope;
+ var normalizedMatch;
+ var i, l;
+ var j, m;
+
+ for (i = 0, l = token[1].length; i < l; i++) {
+ scope = token[1][i];
+ match = scope[1].match(namespaceRegex);
+
+ for (j = 0, m = match.length; j < m; j++) {
+ normalizedMatch = match[j].substring(0, match[j].length - 1);
+
+ if (normalizedMatch in atRules) {
+ delete atRules[normalizedMatch];
+ }
+ }
+ }
+ };
+}
+
+module.exports = removeUnusedAtRules;
removeDuplicateFontRules: true,
removeDuplicateMediaBlocks: true,
removeDuplicateRules: true,
+ removeUnusedAtRules: false,
restructureRules: false,
skipProperties: []
};
--- /dev/null
+var vows = require('vows');
+var optimizerContext = require('../../test-helper').optimizerContext;
+
+vows.describe('remove unused at rules')
+ .addBatch(
+ optimizerContext('@counter-style', {
+ 'one unused declaration': [
+ '@counter-style test{system:fixed;symbols:url(one.png) url(two.png);suffix:" "}',
+ ''
+ ],
+ 'one used declaration in list-style': [
+ '@counter-style test{system:fixed;symbols:url(one.png) url(two.png);suffix:" "}.block{list-style:test}',
+ '@counter-style test{system:fixed;symbols:url(one.png) url(two.png);suffix:" "}.block{list-style:test}'
+ ],
+ 'one used declaration in list-style-type': [
+ '@counter-style test{system:fixed;symbols:url(one.png) url(two.png);suffix:" "}.block{list-style-type:test}',
+ '@counter-style test{system:fixed;symbols:url(one.png) url(two.png);suffix:" "}.block{list-style-type:test}'
+ ],
+ 'one used declaration in nested list-style-type': [
+ '@counter-style test{system:fixed;symbols:url(one.png) url(two.png);suffix:" "}@media screen{.block{list-style-type:test}}',
+ '@counter-style test{system:fixed;symbols:url(one.png) url(two.png);suffix:" "}@media screen{.block{list-style-type:test}}'
+ ],
+ 'one used declaration and one unused': [
+ '@counter-style test{system:fixed;symbols:url(one.png) url(two.png);suffix:" "}@counter-style test2{system:fixed;symbols:url(one.png) url(two.png);suffix:" "}.block{list-style-type:test}',
+ '@counter-style test{system:fixed;symbols:url(one.png) url(two.png);suffix:" "}.block{list-style-type:test}'
+ ]
+ }, { level: { 2: { removeUnusedAtRules: true } } })
+ )
+ .addBatch(
+ optimizerContext('@font-face', {
+ 'one unused declaration': [
+ '@font-face{font-family:test}',
+ ''
+ ],
+ 'one used declaration in font-family': [
+ '@font-face{font-family:test}.block{font-family:test}',
+ '@font-face{font-family:test}.block{font-family:test}'
+ ],
+ 'one used declaration in font-family with different case': [
+ '@font-face{font-family:test}.block{font-family:Test}',
+ '@font-face{font-family:test}.block{font-family:Test}'
+ ],
+ 'one used declaration in multi-valued font-family': [
+ '@font-face{font-family:test}.block{font-family:Arial,test,sans-serif}',
+ '@font-face{font-family:test}.block{font-family:Arial,test,sans-serif}'
+ ],
+ 'one used declaration in font': [
+ '@font-face{font-family:test}.block{font:16px test}',
+ '@font-face{font-family:test}.block{font:16px test}'
+ ],
+ 'one used declaration in multi-valued font': [
+ '@font-face{font-family:test}.block{font:16px Arial,test,sans-serif}',
+ '@font-face{font-family:test}.block{font:16px Arial,test,sans-serif}'
+ ],
+ 'one used declaration in nested font': [
+ '@font-face{font-family:test}@media screen{.block{font:16px test}}',
+ '@font-face{font-family:test}@media screen{.block{font:16px test}}'
+ ],
+ 'one used declaration in multi-valued font with different case': [
+ '@font-face{font-family:test}.block{font:16px Arial,Test,sans-serif}',
+ '@font-face{font-family:test}.block{font:16px Arial,Test,sans-serif}'
+ ],
+ 'one used declaration and one unused': [
+ '@font-face{font-family:test}@font-face{font-family:test2}.block{font:16px test}',
+ '@font-face{font-family:test}.block{font:16px test}'
+ ]
+ }, { level: { 2: { removeUnusedAtRules: true } } })
+ )
+ .addBatch(
+ optimizerContext('@keyframes', {
+ 'one unused declaration': [
+ '@keyframes test{0%{opacity:0}100%{opacity:1}}',
+ ''
+ ],
+ 'one used declaration in animation-name': [
+ '@keyframes test{0%{opacity:0}100%{opacity:1}}.block{animation-name:test}',
+ '@keyframes test{0%{opacity:0}100%{opacity:1}}.block{animation-name:test}'
+ ],
+ 'one used declaration in multi-value animation-name': [
+ '@keyframes test{0%{opacity:0}100%{opacity:1}}.block{animation-name:custom,test}',
+ '@keyframes test{0%{opacity:0}100%{opacity:1}}.block{animation-name:custom,test}'
+ ],
+ 'one used declaration in animation': [
+ '@keyframes test{0%{opacity:0}100%{opacity:1}}.block{animation:1s ease-in test}',
+ '@keyframes test{0%{opacity:0}100%{opacity:1}}.block{animation:1s ease-in test}'
+ ],
+ 'one used declaration in multi-value animation': [
+ '@keyframes test{0%{opacity:0}100%{opacity:1}}.block{animation:1s ease-in custom,2s ease-out test}',
+ '@keyframes test{0%{opacity:0}100%{opacity:1}}.block{animation:1s ease-in custom,2s ease-out test}'
+ ],
+ 'one used declaration in vendor prefixed animation': [
+ '@keyframes test{0%{opacity:0}100%{opacity:1}}.block{-moz-animation:1s ease-in test}',
+ '@keyframes test{0%{opacity:0}100%{opacity:1}}.block{-moz-animation:1s ease-in test}'
+ ],
+ 'one used vendor prefixed declaration in animation': [
+ '@-webkit-keyframes test{0%{opacity:0}100%{opacity:1}}.block{animation:1s ease-in test}',
+ '@-webkit-keyframes test{0%{opacity:0}100%{opacity:1}}.block{animation:1s ease-in test}'
+ ],
+ 'one used in nested animation': [
+ '@keyframes test{0%{opacity:0}100%{opacity:1}}@media screen{.block{animation:1s ease-in test}}',
+ '@keyframes test{0%{opacity:0}100%{opacity:1}}@media screen{.block{animation:1s ease-in test}}'
+ ],
+ 'one used declaration and one unused': [
+ '@keyframes test{0%{opacity:0}100%{opacity:1}}@keyframes test2{0%{opacity:0}100%{opacity:1}}.block{animation-name:test}',
+ '@keyframes test{0%{opacity:0}100%{opacity:1}}.block{animation-name:test}'
+ ]
+ }, { level: { 2: { removeUnusedAtRules: true } } })
+ )
+ .addBatch(
+ optimizerContext('@namespace', {
+ 'one unused declaration': [
+ '@namespace svg url(http://www.w3.org/2000/svg);',
+ ''
+ ],
+ 'one used declaration in scope': [
+ '@namespace svg url(http://www.w3.org/2000/svg);svg|.block{color:red}',
+ '@namespace svg url(http://www.w3.org/2000/svg);svg|.block{color:red}'
+ ],
+ 'one used declaration in attribute': [
+ '@namespace svg url(http://www.w3.org/2000/svg);.block[svg|title=test]{color:red}',
+ '@namespace svg url(http://www.w3.org/2000/svg);.block[svg|title=test]{color:red}'
+ ],
+ 'one used declaration in nested attribute': [
+ '@namespace svg url(http://www.w3.org/2000/svg);@media screen{.block[svg|title=test]{color:red}}',
+ '@namespace svg url(http://www.w3.org/2000/svg);@media screen{.block[svg|title=test]{color:red}}'
+ ],
+ 'many declaration in one rule': [
+ '@namespace svg url(http://www.w3.org/2000/svg);@namespace xlink url(http://www.w3.org/2000/xlink);.block[svg|title=test],xlink|.block{color:red}',
+ '@namespace svg url(http://www.w3.org/2000/svg);@namespace xlink url(http://www.w3.org/2000/xlink);.block[svg|title=test],xlink|.block{color:red}'
+ ],
+ 'one used declaration and one unused': [
+ '@namespace svg url(http://www.w3.org/2000/svg);@namespace xlink url(http://www.w3.org/2000/xlink);svg|.block{color:red}',
+ '@namespace svg url(http://www.w3.org/2000/svg);svg|.block{color:red}'
+ ]
+ }, { level: { 2: { removeUnusedAtRules: true } } })
+ )
+ .export(module);
removeDuplicateFontRules: true,
removeDuplicateMediaBlocks: true,
removeDuplicateRules: true,
+ removeUnusedAtRules: false,
restructureRules: false,
skipProperties: []
});
removeDuplicateFontRules: true,
removeDuplicateMediaBlocks: true,
removeDuplicateRules: true,
+ removeUnusedAtRules: false,
restructureRules: false,
skipProperties: []
});
removeDuplicateFontRules: false,
removeDuplicateMediaBlocks: false,
removeDuplicateRules: false,
+ removeUnusedAtRules: false,
restructureRules: false,
skipProperties: []
});
removeDuplicateFontRules: false,
removeDuplicateMediaBlocks: false,
removeDuplicateRules: false,
+ removeUnusedAtRules: false,
restructureRules: false,
skipProperties: []
});
removeDuplicateFontRules: true,
removeDuplicateMediaBlocks: true,
removeDuplicateRules: true,
+ removeUnusedAtRules: false,
restructureRules: false,
skipProperties: []
});
removeDuplicateFontRules: true,
removeDuplicateMediaBlocks: true,
removeDuplicateRules: true,
+ removeUnusedAtRules: false,
restructureRules: false,
skipProperties: []
});
removeDuplicateFontRules: false,
removeDuplicateMediaBlocks: false,
removeDuplicateRules: false,
+ removeUnusedAtRules: false,
restructureRules: false,
skipProperties: []
});
removeDuplicateFontRules: false,
removeDuplicateMediaBlocks: false,
removeDuplicateRules: false,
+ removeUnusedAtRules: false,
restructureRules: false,
skipProperties: []
});