1/*
2 * Copyright (C) 2013 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26// In the inspector token types have been modified to include extra mode information
27// after the actual token type. So we can't do token === "foo". So instead we do
28// /\bfoo\b/.test(token).
29
30CodeMirror.extendMode("javascript", {
31    shouldHaveSpaceBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
32    {
33        if (!token) {
34            if (content === "(") // Most keywords like "if (" but not "function(" or "typeof(".
35                return lastToken && /\bkeyword\b/.test(lastToken) && (lastContent !== "function" && lastContent !== "typeof" && lastContent !== "instanceof");
36            if (content === ":") // Ternary.
37                return (state.lexical.type === "stat" || state.lexical.type === ")");
38            return false;
39        }
40
41        if (isComment)
42            return true;
43
44        if (/\boperator\b/.test(token)) {
45            if (content === "!") // Unary ! should not be confused with "!=".
46                return false;
47            return "+-/*&&||!===+=-=>=<=?".indexOf(content) >= 0; // Operators.
48        }
49
50        if (/\bkeyword\b/.test(token)) { // Most keywords require spaces before them, unless a '}' can come before it.
51            if (content === "else" || content === "catch" || content === "finally")
52                return lastContent === "}";
53            return false;
54        }
55
56        return false;
57    },
58
59    shouldHaveSpaceAfterLastToken: function(lastToken, lastContent, token, state, content, isComment)
60    {
61        if (lastToken && /\bkeyword\b/.test(lastToken)) {  // Most keywords require spaces after them, unless a '{' or ';' can come after it.
62            if (lastContent === "else")
63                return true;
64            if (lastContent === "catch")
65                return true;
66            if (lastContent === "return")
67                return content !== ";";
68            if (lastContent === "throw")
69                return true;
70            if (lastContent === "try")
71                return true;
72            if (lastContent === "finally")
73                return true;
74            if (lastContent === "do")
75                return true;
76            return false;
77        }
78
79        if (lastToken && /\bcomment\b/.test(lastToken)) // Embedded /* comment */.
80            return true;
81        if (lastContent === ")") // "){".
82            return content === "{";
83        if (lastContent === ";") // In for loop.
84            return state.lexical.type === ")";
85        if (lastContent === "!") // Unary ! should not be confused with "!=".
86            return false;
87
88        return ",+-/*&&||:!===+=-=>=<=?".indexOf(lastContent) >= 0; // Operators.
89    },
90
91    newlinesAfterToken: function(lastToken, lastContent, token, state, content, isComment)
92    {
93        if (!token) {
94            if (content === ",") // In object literals, like in {a:1,b:2}, but not in param lists or vardef lists.
95                return state.lexical.type === "}" ? 1 : 0;
96            if (content === ";") // Everywhere except in for loop conditions.
97                return state.lexical.type !== ")" ? 1 : 0;
98            if (content === ":" && state.lexical.type === "}" && state.lexical.prev && state.lexical.prev.type === "form") // Switch case/default.
99                return 1;
100            return content.length === 1 && "{}".indexOf(content) >= 0 ? 1 : 0; // After braces.
101        }
102
103        if (isComment)
104            return 1;
105
106        return 0;
107    },
108
109    removeLastNewline: function(lastToken, lastContent, token, state, content, isComment, firstTokenOnLine)
110    {
111        if (!token) {
112            if (content === "}") // "{}".
113                return lastContent === "{";
114            if (content === ";") // "x = {};" or ";;".
115                return "};".indexOf(lastContent) >= 0;
116            if (content === ":") // Ternary.
117                return lastContent === "}" && (state.lexical.type === "stat" || state.lexical.type === ")");
118            if (",().".indexOf(content) >= 0) // "})", "}.bind", "function() { ... }()", or "}, false)".
119                return lastContent === "}";
120            return false;
121        }
122
123        if (isComment) { // Comment after semicolon.
124            if (!firstTokenOnLine && lastContent === ";")
125                return true;
126            return false;
127        }
128
129        if (/\bkeyword\b/.test(token)) {
130            if (content === "else" || content === "catch" || content === "finally") // "} else", "} catch", "} finally"
131                return lastContent === "}";
132            return false;
133        }
134
135        return false;
136    },
137
138    indentAfterToken: function(lastToken, lastContent, token, state, content, isComment)
139    {
140        return content === "{" || content === "case" || content === "default";
141    },
142
143    newlineBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
144    {
145        if (state._jsPrettyPrint.shouldIndent)
146            return true;
147
148        return content === "}" && lastContent !== "{"; // "{}"
149    },
150
151    indentBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
152    {
153        if (state._jsPrettyPrint.shouldIndent)
154            return true;
155
156        return false;
157    },
158
159    dedentsBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
160    {
161        var dedent = 0;
162
163        if (state._jsPrettyPrint.shouldDedent)
164            dedent += state._jsPrettyPrint.dedentSize;
165
166        if (!token && content === "}")
167            dedent += 1;
168        else if (token && /\bkeyword\b/.test(token) && (content === "case" || content === "default"))
169            dedent += 1;
170
171        return dedent;
172    },
173
174    modifyStateForTokenPre: function(lastToken, lastContent, token, state, content, isComment)
175    {
176        if (!state._jsPrettyPrint) {
177            state._jsPrettyPrint = {
178                indentCount: 0,       // How far have we indented because of single statement blocks.
179                shouldIndent: false,  // Signal we should indent on entering a single statement block.
180                shouldDedent: false,  // Signal we should dedent on leaving a single statement block.
181                dedentSize: 0,        // How far we should dedent when leaving a single statement block.
182                lastIfIndentCount: 0, // Keep track of the indent the last time we saw an if without braces.
183                openBraceStartMarkers: [],  // Keep track of non-single statement blocks.
184                openBraceTrackingCount: -1, // Keep track of "{" and "}" in non-single statement blocks.
185            };
186        }
187
188        // - Entering:
189        //   - Preconditions:
190        //     - last lexical was a "form" we haven't encountered before
191        //     - last content was ")", "else", or "do"
192        //     - current lexical is not ")" (in an expression or condition)
193        //   - Cases:
194        //     1. "{"
195        //       - indent +0
196        //       - save this indent size so when we encounter the "}" we know how far to dedent
197        //     2. "else if"
198        //       - indent +0 and do not signal to add a newline and indent
199        //       - mark the last if location so when we encounter an "else" we know how far to dedent
200        //       - mark the lexical state so we know we are inside a single statement block
201        //     3. Token without brace.
202        //       - indent +1 and signal to add a newline and indent
203        //       - mark the last if location so when we encounter an "else" we know how far to dedent
204        //       - mark the lexical state so we know we are inside a single statement block
205        if (!isComment && state.lexical.prev && state.lexical.prev.type === "form" && !state.lexical.prev._jsPrettyPrintMarker && (lastContent === ")" || lastContent === "else" || lastContent === "do") && (state.lexical.type !== ")")) {
206            if (content === "{") {
207                // Save the state at the opening brace so we can return to it when we see "}".
208                var savedState = {indentCount:state._jsPrettyPrint.indentCount, openBraceTrackingCount:state._jsPrettyPrint.openBraceTrackingCount};
209                state._jsPrettyPrint.openBraceStartMarkers.push(savedState);
210                state._jsPrettyPrint.openBraceTrackingCount = 1;
211            } else if (state.lexical.type !== "}") {
212                // Increase the indent count. Signal for a newline and indent if needed.
213                if (!(lastContent === "else" && content === "if")) {
214                    state._jsPrettyPrint.indentCount++;
215                    state._jsPrettyPrint.shouldIndent = true;
216                }
217                state.lexical.prev._jsPrettyPrintMarker = true;
218                if (state._jsPrettyPrint.enteringIf)
219                    state._jsPrettyPrint.lastIfIndentCount = state._jsPrettyPrint.indentCount - 1;
220            }
221        }
222
223        // - Leaving:
224        //   - Preconditions:
225        //     - we must be indented
226        //     - ignore ";", wait for the next token instead.
227        //   - Cases:
228        //     1. "else"
229        //       - dedent to the last "if"
230        //     2. "}" and all braces we saw are balanced
231        //       - dedent to the last "{"
232        //     3. Token without a marker on the stack
233        //       - dedent all the way
234        else if (state._jsPrettyPrint.indentCount) {
235            console.assert(!state._jsPrettyPrint.shouldDedent);
236            console.assert(!state._jsPrettyPrint.dedentSize);
237
238            // Track "{" and "}" to know when the "}" is really closing a block.
239            if (!isComment) {
240                if (content === "{")
241                    state._jsPrettyPrint.openBraceTrackingCount++;
242                else if (content === "}")
243                    state._jsPrettyPrint.openBraceTrackingCount--;
244            }
245
246            if (content === ";") {
247                // Ignore.
248            } else if (content === "else") {
249                // Dedent to the last "if".
250                if (lastContent !== "}") {
251                    state._jsPrettyPrint.shouldDedent = true;
252                    state._jsPrettyPrint.dedentSize = state._jsPrettyPrint.indentCount - state._jsPrettyPrint.lastIfIndentCount;
253                    state._jsPrettyPrint.lastIfIndentCount = 0;
254                }
255            } else if (content === "}" && !state._jsPrettyPrint.openBraceTrackingCount && state._jsPrettyPrint.openBraceStartMarkers.length) {
256                // Dedent to the last "{".
257                var savedState = state._jsPrettyPrint.openBraceStartMarkers.pop();
258                state._jsPrettyPrint.shouldDedent = true;
259                state._jsPrettyPrint.dedentSize = state._jsPrettyPrint.indentCount - savedState.indentCount;
260                state._jsPrettyPrint.openBraceTrackingCount = savedState.openBraceTrackingCount;
261            } else {
262                // Dedent all the way.
263                var shouldDedent = true;
264                var lexical = state.lexical.prev;
265                while (lexical) {
266                    if (lexical._jsPrettyPrintMarker) {
267                        shouldDedent = false;
268                        break;
269                    }
270                    lexical = lexical.prev;
271                }
272                if (shouldDedent) {
273                    state._jsPrettyPrint.shouldDedent = true;
274                    state._jsPrettyPrint.dedentSize = state._jsPrettyPrint.indentCount;
275                }
276            }
277        }
278
279        // Signal for when we will be entering an if.
280        if (token && state.lexical.type === "form" && state.lexical.prev && state.lexical.prev !== "form" && /\bkeyword\b/.test(token))
281            state._jsPrettyPrint.enteringIf = (content === "if");
282    },
283
284    modifyStateForTokenPost: function(lastToken, lastContent, token, state, content, isComment)
285    {
286        if (state._jsPrettyPrint.shouldIndent)
287            state._jsPrettyPrint.shouldIndent = false;
288
289        if (state._jsPrettyPrint.shouldDedent) {
290            state._jsPrettyPrint.indentCount -= state._jsPrettyPrint.dedentSize;
291            state._jsPrettyPrint.dedentSize = 0;
292            state._jsPrettyPrint.shouldDedent = false;
293        }
294    }
295});
296
297CodeMirror.extendMode("css", {
298    shouldHaveSpaceBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
299    {
300        if (!token) {
301            if (content === "{")
302                return true;
303            return false;
304        }
305
306        if (isComment)
307            return true;
308
309        if (/\bkeyword\b/.test(token)) {
310            if (content.charAt(0) === "!") // "!important".
311                return true;
312            return false;
313        }
314
315        return false;
316    },
317
318    shouldHaveSpaceAfterLastToken: function(lastToken, lastContent, token, state, content, isComment)
319    {
320        if (!lastToken) {
321            if (lastContent === ",")
322                return true;
323            if (lastContent === ":") // Space in "prop: value" but not in a selectors "a:link" or "div::after" or media queries "(max-device-width:480px)".
324                return state.state === "prop";
325            return false;
326        }
327
328        if (/\bcomment\b/.test(lastToken))
329            return true;
330
331        return false;
332    },
333
334    newlinesAfterToken: function(lastToken, lastContent, token, state, content, isComment)
335    {
336        if (!token) {
337            if (content === ";")
338                return 1;
339            if (content === ",") { // "a,b,c,...,z{}" rule list at top level or in @media top level and only if the line length will be large.
340                if ((state.state === "top" || state.state === "media") && state._cssPrettyPrint.lineLength > 60) {
341                    state._cssPrettyPrint.lineLength = 0;
342                    return 1;
343                }
344                return 0;
345            }
346            if (content === "{")
347                return 1;
348            if (content === "}") // 2 newlines between rule declarations.
349                return 2;
350            return 0;
351        }
352
353        if (isComment)
354            return 1;
355
356        return 0;
357    },
358
359    removeLastNewline: function(lastToken, lastContent, token, state, content, isComment, firstTokenOnLine)
360    {
361        if (isComment) { // Comment after semicolon.
362            if (!firstTokenOnLine && lastContent === ";")
363                return true;
364            return false;
365        }
366
367        return content === "}" && (lastContent === "{" || lastContent === "}"); // "{}" and "}\n}" when closing @media.
368    },
369
370    indentAfterToken: function(lastToken, lastContent, token, state, content, isComment)
371    {
372        return content === "{";
373    },
374
375    newlineBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
376    {
377        return content === "}" && (lastContent !== "{" && lastContent !== "}"); // "{}" and "}\n}" when closing @media.
378    },
379
380    indentBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
381    {
382        return false;
383    },
384
385    dedentsBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
386    {
387        return content === "}" ? 1 : 0;
388    },
389
390    modifyStateForTokenPost: function(lastToken, lastContent, token, state, content, isComment)
391    {
392        if (!state._cssPrettyPrint)
393            state._cssPrettyPrint = {lineLength: 0};
394
395        // In order insert newlines in selector lists we need keep track of the length of the current line.
396        // This isn't exact line length, only the builder knows that, but it is good enough to get an idea.
397        // If we are at a top level, keep track of the current line length, otherwise we reset to 0.
398        if (state.state === "top" || state.state === "media")
399            state._cssPrettyPrint.lineLength += content.length;
400        else
401            state._cssPrettyPrint.lineLength = 0;
402    }
403});
404