From: Jakub Pawlowicz Date: Fri, 30 Dec 2016 21:23:29 +0000 (+0100) Subject: Fixes #829 - adds more strict selector merging rules. X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=2764f78ecb257ebc5d41889124c9b4aaa23f7536;p=clean-css.git Fixes #829 - adds more strict selector merging rules. Why: * Instead of blacklisting unmergeable selectors we're now whitelisting the valid ones and reject all other. --- diff --git a/History.md b/History.md index 2ddfe31f..b610d291 100644 --- a/History.md +++ b/History.md @@ -24,6 +24,7 @@ * 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. diff --git a/lib/optimizer/is-mergeable.js b/lib/optimizer/is-mergeable.js new file mode 100644 index 00000000..ba46e236 --- /dev/null +++ b/lib/optimizer/is-mergeable.js @@ -0,0 +1,189 @@ +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; diff --git a/lib/optimizer/is-special.js b/lib/optimizer/is-special.js deleted file mode 100644 index 96315141..00000000 --- a/lib/optimizer/is-special.js +++ /dev/null @@ -1,5 +0,0 @@ -function isSpecial(options, selector) { - return options.compatibility.selectors.special.test(selector); -} - -module.exports = isSpecial; diff --git a/lib/optimizer/merge-adjacent.js b/lib/optimizer/merge-adjacent.js index 167494bc..e2e0702f 100644 --- a/lib/optimizer/merge-adjacent.js +++ b/lib/optimizer/merge-adjacent.js @@ -3,7 +3,7 @@ var optimizeProperties = require('../properties/optimizer'); 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'); @@ -11,6 +11,8 @@ function mergeAdjacent(tokens, context) { 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]; @@ -26,7 +28,8 @@ function mergeAdjacent(tokens, context) { 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 { diff --git a/lib/optimizer/merge-non-adjacent-by-body.js b/lib/optimizer/merge-non-adjacent-by-body.js index e420784d..546b8c59 100644 --- a/lib/optimizer/merge-non-adjacent-by-body.js +++ b/lib/optimizer/merge-non-adjacent-by-body.js @@ -1,7 +1,7 @@ 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'); @@ -33,6 +33,8 @@ function removeAnyUnsafeElements(left, candidates) { 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--) { @@ -48,7 +50,9 @@ function mergeNonAdjacentByBody(tokens, context) { 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]); diff --git a/lib/optimizer/reduce-non-adjacent.js b/lib/optimizer/reduce-non-adjacent.js index 282cf8d7..f4be6d0d 100644 --- a/lib/optimizer/reduce-non-adjacent.js +++ b/lib/optimizer/reduce-non-adjacent.js @@ -1,13 +1,15 @@ 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 = []; @@ -21,7 +23,8 @@ function reduceNonAdjacent(tokens, context) { } 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) : @@ -80,6 +83,8 @@ function reduceSimpleNonAdjacentCases(tokens, repeated, candidates, options, con } function reduceComplexNonAdjacentCases(tokens, candidates, options, context) { + var mergeablePseudoClasses = options.compatibility.selectors.mergeablePseudoClasses; + var mergeablePseudoElements = options.compatibility.selectors.mergeablePseudoElements; var localContext = {}; function filterOut(idx) { @@ -101,9 +106,9 @@ function reduceComplexNonAdjacentCases(tokens, candidates, options, context) { 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; diff --git a/lib/optimizer/restructure.js b/lib/optimizer/restructure.js index e8753d93..b01d41f8 100644 --- a/lib/optimizer/restructure.js +++ b/lib/optimizer/restructure.js @@ -3,7 +3,7 @@ var canReorderSingle = require('./reorderable').canReorderSingle; 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'); @@ -21,6 +21,8 @@ function cloneAndMergeSelectors(propertyA, propertyB) { 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 = {}; @@ -80,8 +82,9 @@ function restructure(tokens, context) { 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) diff --git a/lib/utils/compatibility.js b/lib/utils/compatibility.js index 1c626649..0733b91a 100644 --- a/lib/utils/compatibility.js +++ b/lib/utils/compatibility.js @@ -1,5 +1,3 @@ -var util = require('util'); - var DEFAULTS = { '*': { colors: { @@ -23,7 +21,42 @@ var DEFAULTS = { 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, @@ -62,7 +95,16 @@ DEFAULTS.ie8 = merge(DEFAULTS.ie9, { 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, @@ -81,7 +123,12 @@ DEFAULTS.ie7 = merge(DEFAULTS.ie8, { }, 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' + ] }, }); @@ -93,10 +140,11 @@ function merge(source, target) { 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; diff --git a/test/optimizer/is-mergeable-test.js b/test/optimizer/is-mergeable-test.js new file mode 100644 index 00000000..0ec17ebd --- /dev/null +++ b/test/optimizer/is-mergeable-test.js @@ -0,0 +1,180 @@ +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); diff --git a/test/optimizer/merge-adjacent-test.js b/test/optimizer/merge-adjacent-test.js index 20a12c31..91a86616 100644 --- a/test/optimizer/merge-adjacent-test.js +++ b/test/optimizer/merge-adjacent-test.js @@ -56,6 +56,10 @@ vows.describe('remove duplicates') '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}'