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