| 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 UNMIXABLE_PSEUDO_CLASSES = [ |
| ':after', |
| ':before', |
| ':first-letter', |
| ':first-line', |
| ':lang' |
| ]; |
| var UNMIXABLE_PSEUDO_ELEMENTS = [ |
| '::after', |
| '::before', |
| '::first-letter', |
| '::first-line' |
| ]; |
| |
| var Level = { |
| DOUBLE_QUOTE: 'double-quote', |
| SINGLE_QUOTE: 'single-quote', |
| ROOT: 'root' |
| }; |
| |
| function isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements, multiplePseudoMerging) { |
| var singleSelectors = split(selector, Marker.COMMA); |
| var singleSelector; |
| var i, l; |
| |
| for (i = 0, l = singleSelectors.length; i < l; i++) { |
| singleSelector = singleSelectors[i]; |
| |
| if (singleSelector.length === 0 || |
| isDeepSelector(singleSelector) || |
| (singleSelector.indexOf(Marker.COLON) > -1 && !areMergeable(singleSelector, extractPseudoFrom(singleSelector), mergeablePseudoClasses, mergeablePseudoElements, multiplePseudoMerging))) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| 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 (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 (isQuoted) { |
| buffer.push(character); |
| } 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 areMergeable(selector, matches, mergeablePseudoClasses, mergeablePseudoElements, multiplePseudoMerging) { |
| return areAllowed(matches, mergeablePseudoClasses, mergeablePseudoElements) && |
| needArguments(matches) && |
| (matches.length < 2 || !someIncorrectlyChained(selector, matches)) && |
| (matches.length < 2 || multiplePseudoMerging && allMixable(matches)); |
| } |
| |
| function areAllowed(matches, mergeablePseudoClasses, mergeablePseudoElements) { |
| var match; |
| var name; |
| var i, l; |
| |
| for (i = 0, l = matches.length; i < l; i++) { |
| match = matches[i]; |
| name = match.indexOf(Marker.OPEN_ROUND_BRACKET) > -1 ? |
| match.substring(0, match.indexOf(Marker.OPEN_ROUND_BRACKET)) : |
| match; |
| |
| if (mergeablePseudoClasses.indexOf(name) === -1 && mergeablePseudoElements.indexOf(name) === -1) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| function needArguments(matches) { |
| var match; |
| var name; |
| var bracketOpensAt; |
| var hasArguments; |
| var i, l; |
| |
| for (i = 0, l = matches.length; i < l; i++) { |
| match = matches[i]; |
| |
| bracketOpensAt = match.indexOf(Marker.OPEN_ROUND_BRACKET); |
| hasArguments = bracketOpensAt > -1; |
| name = hasArguments ? |
| match.substring(0, bracketOpensAt) : |
| match; |
| |
| if (hasArguments && PSEUDO_CLASSES_WITH_ARGUMENTS.indexOf(name) == -1) { |
| return false; |
| } |
| |
| if (!hasArguments && PSEUDO_CLASSES_WITH_ARGUMENTS.indexOf(name) > -1) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| function someIncorrectlyChained(selector, matches) { |
| var positionInSelector = 0; |
| var match; |
| var matchAt; |
| var nextMatch; |
| var nextMatchAt; |
| var name; |
| var nextName; |
| var areChained; |
| var i, l; |
| |
| for (i = 0, l = matches.length; i < l; i++) { |
| match = matches[i]; |
| nextMatch = matches[i + 1]; |
| |
| if (!nextMatch) { |
| break; |
| } |
| |
| 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; |
| |
| if (name != NOT_PSEUDO || nextName != NOT_PSEUDO) { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| function allMixable(matches) { |
| var unmixableMatches = 0; |
| var match; |
| var i, l; |
| |
| for (i = 0, l = matches.length; i < l; i++) { |
| match = matches[i]; |
| |
| if (isPseudoElement(match)) { |
| unmixableMatches += UNMIXABLE_PSEUDO_ELEMENTS.indexOf(match) > -1 ? 1 : 0; |
| } else { |
| unmixableMatches += UNMIXABLE_PSEUDO_CLASSES.indexOf(match) > -1 ? 1 : 0; |
| } |
| |
| if (unmixableMatches > 1) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| function isPseudoElement(pseudo) { |
| return DOUBLE_COLON_PATTERN.test(pseudo); |
| } |
| |
| module.exports = isMergeable; |