1var Marker = require('../../tokenizer/marker'); 2var split = require('../../utils/split'); 3 4var DEEP_SELECTOR_PATTERN = /\/deep\//; 5var DOUBLE_COLON_PATTERN = /^::/; 6var NOT_PSEUDO = ':not'; 7var PSEUDO_CLASSES_WITH_ARGUMENTS = [ 8 ':dir', 9 ':lang', 10 ':not', 11 ':nth-child', 12 ':nth-last-child', 13 ':nth-last-of-type', 14 ':nth-of-type' 15]; 16var RELATION_PATTERN = /[>\+~]/; 17var UNMIXABLE_PSEUDO_CLASSES = [ 18 ':after', 19 ':before', 20 ':first-letter', 21 ':first-line', 22 ':lang' 23]; 24var UNMIXABLE_PSEUDO_ELEMENTS = [ 25 '::after', 26 '::before', 27 '::first-letter', 28 '::first-line' 29]; 30 31var Level = { 32 DOUBLE_QUOTE: 'double-quote', 33 SINGLE_QUOTE: 'single-quote', 34 ROOT: 'root' 35}; 36 37function isMergeable(selector, mergeablePseudoClasses, mergeablePseudoElements, multiplePseudoMerging) { 38 var singleSelectors = split(selector, Marker.COMMA); 39 var singleSelector; 40 var i, l; 41 42 for (i = 0, l = singleSelectors.length; i < l; i++) { 43 singleSelector = singleSelectors[i]; 44 45 if (singleSelector.length === 0 || 46 isDeepSelector(singleSelector) || 47 (singleSelector.indexOf(Marker.COLON) > -1 && !areMergeable(singleSelector, extractPseudoFrom(singleSelector), mergeablePseudoClasses, mergeablePseudoElements, multiplePseudoMerging))) { 48 return false; 49 } 50 } 51 52 return true; 53} 54 55function isDeepSelector(selector) { 56 return DEEP_SELECTOR_PATTERN.test(selector); 57} 58 59function extractPseudoFrom(selector) { 60 var list = []; 61 var character; 62 var buffer = []; 63 var level = Level.ROOT; 64 var roundBracketLevel = 0; 65 var isQuoted; 66 var isEscaped; 67 var isPseudo = false; 68 var isRelation; 69 var wasColon = false; 70 var index; 71 var len; 72 73 for (index = 0, len = selector.length; index < len; index++) { 74 character = selector[index]; 75 76 isRelation = !isEscaped && RELATION_PATTERN.test(character); 77 isQuoted = level == Level.DOUBLE_QUOTE || level == Level.SINGLE_QUOTE; 78 79 if (isEscaped) { 80 buffer.push(character); 81 } else if (character == Marker.DOUBLE_QUOTE && level == Level.ROOT) { 82 buffer.push(character); 83 level = Level.DOUBLE_QUOTE; 84 } else if (character == Marker.DOUBLE_QUOTE && level == Level.DOUBLE_QUOTE) { 85 buffer.push(character); 86 level = Level.ROOT; 87 } else if (character == Marker.SINGLE_QUOTE && level == Level.ROOT) { 88 buffer.push(character); 89 level = Level.SINGLE_QUOTE; 90 } else if (character == Marker.SINGLE_QUOTE && level == Level.SINGLE_QUOTE) { 91 buffer.push(character); 92 level = Level.ROOT; 93 } else if (isQuoted) { 94 buffer.push(character); 95 } else if (character == Marker.OPEN_ROUND_BRACKET) { 96 buffer.push(character); 97 roundBracketLevel++; 98 } else if (character == Marker.CLOSE_ROUND_BRACKET && roundBracketLevel == 1 && isPseudo) { 99 buffer.push(character); 100 list.push(buffer.join('')); 101 roundBracketLevel--; 102 buffer = []; 103 isPseudo = false; 104 } else if (character == Marker.CLOSE_ROUND_BRACKET) { 105 buffer.push(character); 106 roundBracketLevel--; 107 } else if (character == Marker.COLON && roundBracketLevel === 0 && isPseudo && !wasColon) { 108 list.push(buffer.join('')); 109 buffer = []; 110 buffer.push(character); 111 } else if (character == Marker.COLON && roundBracketLevel === 0 && !wasColon) { 112 buffer = []; 113 buffer.push(character); 114 isPseudo = true; 115 } else if (character == Marker.SPACE && roundBracketLevel === 0 && isPseudo) { 116 list.push(buffer.join('')); 117 buffer = []; 118 isPseudo = false; 119 } else if (isRelation && roundBracketLevel === 0 && isPseudo) { 120 list.push(buffer.join('')); 121 buffer = []; 122 isPseudo = false; 123 } else { 124 buffer.push(character); 125 } 126 127 isEscaped = character == Marker.BACK_SLASH; 128 wasColon = character == Marker.COLON; 129 } 130 131 if (buffer.length > 0 && isPseudo) { 132 list.push(buffer.join('')); 133 } 134 135 return list; 136} 137 138function areMergeable(selector, matches, mergeablePseudoClasses, mergeablePseudoElements, multiplePseudoMerging) { 139 return areAllowed(matches, mergeablePseudoClasses, mergeablePseudoElements) && 140 needArguments(matches) && 141 (matches.length < 2 || !someIncorrectlyChained(selector, matches)) && 142 (matches.length < 2 || multiplePseudoMerging && allMixable(matches)); 143} 144 145function areAllowed(matches, mergeablePseudoClasses, mergeablePseudoElements) { 146 var match; 147 var name; 148 var i, l; 149 150 for (i = 0, l = matches.length; i < l; i++) { 151 match = matches[i]; 152 name = match.indexOf(Marker.OPEN_ROUND_BRACKET) > -1 ? 153 match.substring(0, match.indexOf(Marker.OPEN_ROUND_BRACKET)) : 154 match; 155 156 if (mergeablePseudoClasses.indexOf(name) === -1 && mergeablePseudoElements.indexOf(name) === -1) { 157 return false; 158 } 159 } 160 161 return true; 162} 163 164function needArguments(matches) { 165 var match; 166 var name; 167 var bracketOpensAt; 168 var hasArguments; 169 var i, l; 170 171 for (i = 0, l = matches.length; i < l; i++) { 172 match = matches[i]; 173 174 bracketOpensAt = match.indexOf(Marker.OPEN_ROUND_BRACKET); 175 hasArguments = bracketOpensAt > -1; 176 name = hasArguments ? 177 match.substring(0, bracketOpensAt) : 178 match; 179 180 if (hasArguments && PSEUDO_CLASSES_WITH_ARGUMENTS.indexOf(name) == -1) { 181 return false; 182 } 183 184 if (!hasArguments && PSEUDO_CLASSES_WITH_ARGUMENTS.indexOf(name) > -1) { 185 return false; 186 } 187 } 188 189 return true; 190} 191 192function someIncorrectlyChained(selector, matches) { 193 var positionInSelector = 0; 194 var match; 195 var matchAt; 196 var nextMatch; 197 var nextMatchAt; 198 var name; 199 var nextName; 200 var areChained; 201 var i, l; 202 203 for (i = 0, l = matches.length; i < l; i++) { 204 match = matches[i]; 205 nextMatch = matches[i + 1]; 206 207 if (!nextMatch) { 208 break; 209 } 210 211 matchAt = selector.indexOf(match, positionInSelector); 212 nextMatchAt = selector.indexOf(match, matchAt + 1); 213 positionInSelector = nextMatchAt; 214 areChained = matchAt + match.length == nextMatchAt; 215 216 if (areChained) { 217 name = match.indexOf(Marker.OPEN_ROUND_BRACKET) > -1 ? 218 match.substring(0, match.indexOf(Marker.OPEN_ROUND_BRACKET)) : 219 match; 220 nextName = nextMatch.indexOf(Marker.OPEN_ROUND_BRACKET) > -1 ? 221 nextMatch.substring(0, nextMatch.indexOf(Marker.OPEN_ROUND_BRACKET)) : 222 nextMatch; 223 224 if (name != NOT_PSEUDO || nextName != NOT_PSEUDO) { 225 return true; 226 } 227 } 228 } 229 230 return false; 231} 232 233function allMixable(matches) { 234 var unmixableMatches = 0; 235 var match; 236 var i, l; 237 238 for (i = 0, l = matches.length; i < l; i++) { 239 match = matches[i]; 240 241 if (isPseudoElement(match)) { 242 unmixableMatches += UNMIXABLE_PSEUDO_ELEMENTS.indexOf(match) > -1 ? 1 : 0; 243 } else { 244 unmixableMatches += UNMIXABLE_PSEUDO_CLASSES.indexOf(match) > -1 ? 1 : 0; 245 } 246 247 if (unmixableMatches > 1) { 248 return false; 249 } 250 } 251 252 return true; 253} 254 255function isPseudoElement(pseudo) { 256 return DOUBLE_COLON_PATTERN.test(pseudo); 257} 258 259module.exports = isMergeable; 260