* Fixed issue [#801](https://github.com/jakubpawlowicz/clean-css/issues/801) - smarter `@import` inlining.
* Fixed issue [#817](https://github.com/jakubpawlowicz/clean-css/issues/817) - makes `off` disable rounding.
* Fixed issue [#818](https://github.com/jakubpawlowicz/clean-css/issues/818) - disables `px` rounding by default.
+* Fixed issue [#829](https://github.com/jakubpawlowicz/clean-css/issues/829) - adds more strict selector merging rules.
* Fixed issue [#834](https://github.com/jakubpawlowicz/clean-css/issues/834) - adds extra line break in nested blocks.
* Fixed issue [#839](https://github.com/jakubpawlowicz/clean-css/issues/839) - allows URIs in import inlining rules.
* Fixed issue [#840](https://github.com/jakubpawlowicz/clean-css/issues/840) - allows input source map as map object.
--- /dev/null
+var Marker = require('../tokenizer/marker');
+var split = require('../utils/split');
+
+var DEEP_SELECTOR_PATTERN = /\/deep\//;
+var DOUBLE_COLON_PATTERN = /^::/;
+var NOT_PSEUDO = ':not';
+var PSEUDO_CLASSES_WITH_ARGUMENTS = [
+ ':dir',
+ ':lang',
+ ':not',
+ ':nth-child',
+ ':nth-last-child',
+ ':nth-last-of-type',
+ ':nth-of-type'
+];
+var RELATION_PATTERN = /[>\+~]/;
+
+var Level = {
+ DOUBLE_QUOTE: 'double-quote',
+ SINGLE_QUOTE: 'single-quote',
+ ROOT: 'root'
+};
+
+function isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements) {
+ return split(selector, Marker.COMMA).every(function (singleSelector) {
+ return singleSelector.length > 0 &&
+ !isDeepSelector(singleSelector) &&
+ areMergable(singleSelector, extractPseudoFrom(singleSelector), mergeablePseudoClasses, mergeablePseudoElements);
+ });
+}
+
+function isDeepSelector(selector) {
+ return DEEP_SELECTOR_PATTERN.test(selector);
+}
+
+function extractPseudoFrom(selector) {
+ var list = [];
+ var character;
+ var buffer = [];
+ var level = Level.ROOT;
+ var roundBracketLevel = 0;
+ var isQuoted;
+ var isEscaped;
+ var isPseudo = false;
+ var isRelation;
+ var wasColon = false;
+ var index;
+ var len;
+
+ for (index = 0, len = selector.length; index < len; index++) {
+ character = selector[index];
+
+ isRelation = !isEscaped && RELATION_PATTERN.test(character);
+ isQuoted = level == Level.DOUBLE_QUOTE || level == Level.SINGLE_QUOTE;
+
+ if (isEscaped) {
+ buffer.push(character);
+ } else if (isQuoted) {
+ buffer.push(character);
+ } else if (character == Marker.DOUBLE_QUOTE && level == Level.ROOT) {
+ buffer.push(character);
+ level = Level.DOUBLE_QUOTE;
+ } else if (character == Marker.DOUBLE_QUOTE && level == Level.DOUBLE_QUOTE) {
+ buffer.push(character);
+ level = Level.ROOT;
+ } else if (character == Marker.SINGLE_QUOTE && level == Level.ROOT) {
+ buffer.push(character);
+ level = Level.SINGLE_QUOTE;
+ } else if (character == Marker.SINGLE_QUOTE && level == Level.SINGLE_QUOTE) {
+ buffer.push(character);
+ level = Level.ROOT;
+ } else if (character == Marker.OPEN_ROUND_BRACKET) {
+ buffer.push(character);
+ roundBracketLevel++;
+ } else if (character == Marker.CLOSE_ROUND_BRACKET && roundBracketLevel == 1 && isPseudo) {
+ buffer.push(character);
+ list.push(buffer.join(''));
+ roundBracketLevel--;
+ buffer = [];
+ isPseudo = false;
+ } else if (character == Marker.CLOSE_ROUND_BRACKET) {
+ buffer.push(character);
+ roundBracketLevel--;
+ } else if (character == Marker.COLON && roundBracketLevel === 0 && isPseudo && !wasColon) {
+ list.push(buffer.join(''));
+ buffer = [];
+ buffer.push(character);
+ } else if (character == Marker.COLON && roundBracketLevel === 0 && !wasColon) {
+ buffer = [];
+ buffer.push(character);
+ isPseudo = true;
+ } else if (character == Marker.SPACE && roundBracketLevel === 0 && isPseudo) {
+ list.push(buffer.join(''));
+ buffer = [];
+ isPseudo = false;
+ } else if (isRelation && roundBracketLevel === 0 && isPseudo) {
+ list.push(buffer.join(''));
+ buffer = [];
+ isPseudo = false;
+ } else {
+ buffer.push(character);
+ }
+
+ isEscaped = character == Marker.BACK_SLASH;
+ wasColon = character == Marker.COLON;
+ }
+
+ if (buffer.length > 0 && isPseudo) {
+ list.push(buffer.join(''));
+ }
+
+ return list;
+}
+
+function areMergable(selector, matches, mergeablePseudoClasses, mergeablePseudoElements) {
+ return areAllowed(matches, mergeablePseudoClasses, mergeablePseudoElements) &&
+ needArguments(matches) &&
+ !someIncorrectlyChained(selector, matches) &&
+ !someMixed(matches);
+}
+
+function areAllowed(matches, mergeablePseudoClasses, mergeablePseudoElements) {
+ return matches.every(function (match) {
+ var name = match.indexOf(Marker.OPEN_ROUND_BRACKET) > -1 ?
+ match.substring(0, match.indexOf(Marker.OPEN_ROUND_BRACKET)) :
+ match;
+
+ return mergeablePseudoClasses.indexOf(name) > -1 || mergeablePseudoElements.indexOf(name) > -1;
+ });
+}
+
+function needArguments(matches) {
+ return matches.every(function (match) {
+ var bracketOpensAt = match.indexOf(Marker.OPEN_ROUND_BRACKET);
+ var hasArguments = bracketOpensAt > -1;
+ var name = hasArguments ?
+ match.substring(0, bracketOpensAt) :
+ match;
+
+ return hasArguments ?
+ PSEUDO_CLASSES_WITH_ARGUMENTS.indexOf(name) > -1 :
+ PSEUDO_CLASSES_WITH_ARGUMENTS.indexOf(name) == -1;
+ });
+}
+
+function someIncorrectlyChained(selector, matches) {
+ var positionInSelector = 0;
+
+ return matches.some(function (match, index) {
+ var matchAt;
+ var nextMatch = matches[index + 1];
+ var nextMatchAt;
+ var name;
+ var nextName;
+ var areChained;
+
+ if (!nextMatch) {
+ return false;
+ }
+
+ matchAt = selector.indexOf(match, positionInSelector);
+ nextMatchAt = selector.indexOf(match, matchAt + 1);
+ positionInSelector = nextMatchAt;
+ areChained = matchAt + match.length == nextMatchAt;
+
+ if (areChained) {
+ name = match.indexOf(Marker.OPEN_ROUND_BRACKET) > -1 ?
+ match.substring(0, match.indexOf(Marker.OPEN_ROUND_BRACKET)) :
+ match;
+ nextName = nextMatch.indexOf(Marker.OPEN_ROUND_BRACKET) > -1 ?
+ nextMatch.substring(0, nextMatch.indexOf(Marker.OPEN_ROUND_BRACKET)) :
+ nextMatch;
+
+ return name != NOT_PSEUDO || nextName != NOT_PSEUDO;
+ } else {
+ return false;
+ }
+ });
+}
+
+function someMixed(matches) {
+ var firstIsPseudoElement = DOUBLE_COLON_PATTERN.test(matches[0]);
+
+ return matches.some(function (match) {
+ return DOUBLE_COLON_PATTERN.test(match) != firstIsPseudoElement;
+ });
+}
+
+module.exports = isMergeable;
+++ /dev/null
-function isSpecial(options, selector) {
- return options.compatibility.selectors.special.test(selector);
-}
-
-module.exports = isSpecial;
var serializeBody = require('../writer/one-time').body;
var serializeRules = require('../writer/one-time').rules;
var tidyRules = require('./tidy-rules');
-var isSpecial = require('./is-special');
+var isMergeable = require('./is-mergeable');
var Token = require('../tokenizer/token');
var lastToken = [null, [], []];
var options = context.options;
var adjacentSpace = options.compatibility.selectors.adjacentSpace;
+ var mergeablePseudoClasses = options.compatibility.selectors.mergeablePseudoClasses;
+ var mergeablePseudoElements = options.compatibility.selectors.mergeablePseudoElements;
for (var i = 0, l = tokens.length; i < l; i++) {
var token = tokens[i];
optimizeProperties(token[1], lastToken[2], joinAt, true, context);
token[2] = [];
} else if (lastToken[0] == Token.RULE && serializeBody(token[2]) == serializeBody(lastToken[2]) &&
- !isSpecial(options, serializeRules(token[1])) && !isSpecial(options, serializeRules(lastToken[1]))) {
+ isMergeable(serializeRules(token[1]), mergeablePseudoClasses, mergeablePseudoElements) &&
+ isMergeable(serializeRules(lastToken[1]), mergeablePseudoClasses, mergeablePseudoElements)) {
lastToken[1] = tidyRules(lastToken[1].concat(token[1]), false, adjacentSpace, false, context.warnings);
token[2] = [];
} else {
var serializeBody = require('../writer/one-time').body;
var serializeRules = require('../writer/one-time').rules;
var tidyRules = require('./tidy-rules');
-var isSpecial = require('./is-special');
+var isMergeable = require('./is-mergeable');
var Token = require('../tokenizer/token');
function mergeNonAdjacentByBody(tokens, context) {
var options = context.options;
var adjacentSpace = options.compatibility.selectors.adjacentSpace;
+ var mergeablePseudoClasses = options.compatibility.selectors.mergeablePseudoClasses;
+ var mergeablePseudoElements = options.compatibility.selectors.mergeablePseudoElements;
var candidates = {};
for (var i = tokens.length - 1; i >= 0; i--) {
var candidateBody = serializeBody(token[2]);
var oldToken = candidates[candidateBody];
- if (oldToken && !isSpecial(options, serializeRules(token[1])) && !isSpecial(options, serializeRules(oldToken[1]))) {
+ if (oldToken &&
+ isMergeable(serializeRules(token[1]), mergeablePseudoClasses, mergeablePseudoElements) &&
+ isMergeable(serializeRules(oldToken[1]), mergeablePseudoClasses, mergeablePseudoElements)) {
token[1] = token[2].length > 0 ?
tidyRules(oldToken[1].concat(token[1]), false, adjacentSpace, false, context.warnings) :
oldToken[1].concat(token[1]);
var optimizeProperties = require('../properties/optimizer');
var serializeBody = require('../writer/one-time').body;
var serializeRules = require('../writer/one-time').rules;
-var isSpecial = require('./is-special');
+var isMergeable = require('./is-mergeable');
var cloneArray = require('../utils/clone-array');
var Token = require('../tokenizer/token');
function reduceNonAdjacent(tokens, context) {
var options = context.options;
+ var mergeablePseudoClasses = options.compatibility.selectors.mergeablePseudoClasses;
+ var mergeablePseudoElements = options.compatibility.selectors.mergeablePseudoElements;
var candidates = {};
var repeated = [];
}
var selectorAsString = serializeRules(token[1]);
- var isComplexAndNotSpecial = token[1].length > 1 && !isSpecial(options, selectorAsString);
+ var isComplexAndNotSpecial = token[1].length > 1 &&
+ isMergeable(selectorAsString, mergeablePseudoClasses, mergeablePseudoElements);
var wrappedSelectors = wrappedSelectorsFrom(token[1]);
var selectors = isComplexAndNotSpecial ?
[selectorAsString].concat(wrappedSelectors) :
}
function reduceComplexNonAdjacentCases(tokens, candidates, options, context) {
+ var mergeablePseudoClasses = options.compatibility.selectors.mergeablePseudoClasses;
+ var mergeablePseudoElements = options.compatibility.selectors.mergeablePseudoElements;
var localContext = {};
function filterOut(idx) {
var intoToken = tokens[intoPosition];
var reducedBodies = [];
- var selectors = isSpecial(options, complexSelector) ?
- [complexSelector] :
- into[0].list;
+ var selectors = isMergeable(complexSelector, mergeablePseudoClasses, mergeablePseudoElements) ?
+ into[0].list :
+ [complexSelector];
localContext.intoPosition = intoPosition;
localContext.reducedBodies = reducedBodies;
var serializeBody = require('../writer/one-time').body;
var serializeRules = require('../writer/one-time').rules;
var tidyRuleDuplicates = require('./tidy-rule-duplicates');
-var isSpecial = require('./is-special');
+var isMergeable = require('./is-mergeable');
var cloneArray = require('../utils/clone-array');
var Token = require('../tokenizer/token');
function restructure(tokens, context) {
var options = context.options;
+ var mergeablePseudoClasses = options.compatibility.selectors.mergeablePseudoClasses;
+ var mergeablePseudoElements = options.compatibility.selectors.mergeablePseudoElements;
var movableTokens = {};
var movedProperties = [];
var multiPropertyMoveCache = {};
var mergeableTokens = [];
for (var i = sourceTokens.length - 1; i >= 0; i--) {
- if (isSpecial(options, serializeRules(sourceTokens[i][1])))
+ if (!isMergeable(serializeRules(sourceTokens[i][1]), mergeablePseudoClasses, mergeablePseudoElements)) {
continue;
+ }
mergeableTokens.unshift(sourceTokens[i]);
if (sourceTokens[i][2].length > 0 && uniqueTokensWithBody.indexOf(sourceTokens[i]) == -1)
-var util = require('util');
-
var DEFAULTS = {
'*': {
colors: {
selectors: {
adjacentSpace: false, // div+ nav Android stock browser hack
ie7Hack: false, // *+html hack
- special: /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:dir\([a-z-]*\)|:first(?![a-z-])|:fullscreen|:left|:read-only|:read-write|:right|:placeholder|:host|:content|\/deep\/|:shadow|:selection|^,)/ // special selectors which prevent merging
+ mergeablePseudoClasses: [
+ ':active',
+ ':after',
+ ':before',
+ ':empty',
+ ':checked',
+ ':disabled',
+ ':empty',
+ ':enabled',
+ ':first-child',
+ ':first-letter',
+ ':first-line',
+ ':first-of-type',
+ ':focus',
+ ':hover',
+ ':lang',
+ ':last-child',
+ ':last-of-type',
+ ':link',
+ ':not',
+ ':nth-child',
+ ':nth-last-child',
+ ':nth-last-of-type',
+ ':nth-of-type',
+ ':only-child',
+ ':only-of-type',
+ ':root',
+ ':target',
+ ':visited'
+ ], // selectors with these pseudo-classes can be merged as these are universally supported
+ mergeablePseudoElements: [
+ '::after',
+ '::before',
+ '::first-letter',
+ '::first-line'
+ ] // selectors with these pseudo-elements can be merged as these are universally supported
},
units: {
ch: true,
merging: false
},
selectors: {
- special: /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not|:placeholder|:host|::content|\/deep\/|::shadow|^,)/
+ mergeablePseudoClasses: [
+ ':after',
+ ':before',
+ ':first-child',
+ ':first-letter',
+ ':focus',
+ ':hover',
+ ':visited'
+ ],
+ mergeablePseudoElements: []
},
units: {
ch: false,
},
selectors: {
ie7Hack: true,
- special: /(\-moz\-|\-ms\-|\-o\-|\-webkit\-|:focus|:before|:after|:root|:nth|:first\-of|:last|:only|:empty|:target|:checked|::selection|:enabled|:disabled|:not|:placeholder|:host|::content|\/deep\/|::shadow|^,)/
+ mergeablePseudoClasses: [
+ ':first-child',
+ ':first-letter',
+ ':hover',
+ ':visited'
+ ]
},
});
for (var key in source) {
var value = source[key];
- if (typeof value === 'object' && !util.isRegExp(value))
+ if (typeof value === 'object' && !Array.isArray(value)) {
target[key] = merge(value, target[key] || {});
- else
+ } else {
target[key] = key in target ? target[key] : value;
+ }
}
return target;
--- /dev/null
+var assert = require('assert');
+
+var vows = require('vows');
+
+var isMergeable = require('../../lib/optimizer/is-mergeable');
+var mergeablePseudoClasses = [':after'];
+var mergeablePseudoElements = ['::before'];
+
+vows.describe(isMergeable)
+ .addBatch({
+ 'tag name selector': {
+ 'topic': 'div',
+ 'is mergeable': function (selector) {
+ assert.isTrue(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'class selector': {
+ 'topic': '.class',
+ 'is mergeable': function (selector) {
+ assert.isTrue(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'id selector': {
+ 'topic': '#id',
+ 'is mergeable': function (selector) {
+ assert.isTrue(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'complex selector': {
+ 'topic': 'div ~ #id > .class',
+ 'is mergeable': function (selector) {
+ assert.isTrue(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'vendor-prefixed pseudo-class': {
+ 'topic': ':-moz-placeholder',
+ 'is not mergeable': function (selector) {
+ assert.isFalse(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'vendor-prefixed pseudo-element': {
+ 'topic': '::-moz-placeholder',
+ 'is not mergeable': function (selector) {
+ assert.isFalse(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'unsupported pseudo-class': {
+ 'topic': ':first-child',
+ 'is not mergeable': function (selector) {
+ assert.isFalse(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'unsupported pseudo-element': {
+ 'topic': '::marker',
+ 'is not mergeable': function (selector) {
+ assert.isFalse(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'supported pseudo-class': {
+ 'topic': ':after',
+ 'is mergeable': function (selector) {
+ assert.isTrue(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'supported pseudo-class with selector': {
+ 'topic': 'div:after',
+ 'is mergeable': function (selector) {
+ assert.isTrue(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'supported pseudo-class with arguments': {
+ 'topic': 'div:lang(en)',
+ 'is mergeable': function (selector) {
+ assert.isTrue(isMergeable(selector, [':lang'], mergeablePseudoElements));
+ }
+ },
+ 'supported pseudo-class in the middle': {
+ 'topic': 'div :first-child > span',
+ 'is mergeable': function (selector) {
+ assert.isTrue(isMergeable(selector, [':first-child'], mergeablePseudoElements));
+ }
+ },
+ 'supported pseudo-classes in the middle': {
+ 'topic': 'div :first-child > span:last-child > em',
+ 'is mergeable': function (selector) {
+ assert.isTrue(isMergeable(selector, [':first-child', ':last-child'], mergeablePseudoElements));
+ }
+ },
+ 'supported pseudo-classes in the middle without spaces': {
+ 'topic': 'div :first-child>span:last-child>em',
+ 'is mergeable': function (selector) {
+ assert.isTrue(isMergeable(selector, [':first-child', ':last-child'], mergeablePseudoElements));
+ }
+ },
+ 'double :not pseudo-class': {
+ 'topic': 'div:not(:first-child):not(.one)',
+ 'is mergeable': function (selector) {
+ assert.isTrue(isMergeable(selector, [':first-child', ':not'], mergeablePseudoElements));
+ }
+ },
+ 'supported pseudo-class with unsupported arguments': {
+ 'topic': 'div:after(test)',
+ 'is not mergeable': function (selector) {
+ assert.isFalse(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'supported pseudo-class repeated': {
+ 'topic': 'div:after:after',
+ 'is not mergeable': function (selector) {
+ assert.isFalse(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'supported pseudo-element': {
+ 'topic': '::before',
+ 'is mergeable': function (selector) {
+ assert.isTrue(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'supported pseudo-element with selector': {
+ 'topic': 'div::before',
+ 'is mergeable': function (selector) {
+ assert.isTrue(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'supported pseudo-element with arguments': {
+ 'topic': '::before(test)',
+ 'is not mergeable': function (selector) {
+ assert.isFalse(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'supported pseudo-element repeated': {
+ 'topic': '::before::before',
+ 'is not mergeable': function (selector) {
+ assert.isFalse(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'supported pseudo-class and -element mixed': {
+ 'topic': ':after::before',
+ 'is not mergeable': function (selector) {
+ assert.isFalse(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'supported pseudo-element and -class mixed': {
+ 'topic': '::before:after',
+ 'is not mergeable': function (selector) {
+ assert.isFalse(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ '/deep/ selector': {
+ 'topic': '.wrapper /deep/ a',
+ 'is not mergeable': function (selector) {
+ assert.isFalse(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'empty selector': {
+ 'topic': '',
+ 'is not mergeable': function (selector) {
+ assert.isFalse(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'multi selector': {
+ 'topic': 'h1,div',
+ 'is mergeable': function (selector) {
+ assert.isTrue(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'multi selector with pseudo-class': {
+ 'topic': 'h1:first-child,div',
+ 'is not mergeable': function (selector) {
+ assert.isFalse(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ },
+ 'multi selector with empty': {
+ 'topic': ',h1',
+ 'is not mergeable': function (selector) {
+ assert.isFalse(isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements));
+ }
+ }
+ })
+ .export(module);
'a{color:red}::-webkit-scrollbar,a{color:#fff}',
'a{color:red}::-webkit-scrollbar,a{color:#fff}'
],
+ 'when an invalid pseudo selector used': [
+ 'a{color:red}a:after:after{color:red}',
+ 'a{color:red}a:after:after{color:red}'
+ ],
'two same selectors over a block': [
'.one{color:red}@media print{.two{display:block}}.one{display:none}',
'@media print{.two{display:block}}.one{color:red;display:none}'