Web Inspector: Add PrettyPrinter CSS tests
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / CodeMirrorFormatters.js
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
30 CodeMirror.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
297 CodeMirror.extendMode("css", {
298     shouldHaveSpaceBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
299     {
300         if (!token) {
301             if (content === "{")
302                 return true;
303             return ">+~-*/".indexOf(content) >= 0; // calc() expression or child/sibling selectors
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             if (lastContent === ")" && (content !== ")" && content !== ",")) // Space in "not(foo)and" but not at the end of "not(not(foo))"
326                 return state.state === "media" || state.state === "media_parens";
327             return ">+~-*/".indexOf(lastContent) >= 0; // calc() expression or child/sibling selectors
328         }
329
330         if (/\bcomment\b/.test(lastToken))
331             return true;
332
333         if (/\bkeyword\b/.test(lastToken)) // media-query keywords
334             return state.state === "media" || state.state === "media_parens";
335
336         return false;
337     },
338
339     newlinesAfterToken: function(lastToken, lastContent, token, state, content, isComment)
340     {
341         if (!token) {
342             if (content === ";")
343                 return 1;
344             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.
345                 if ((state.state === "top" || state.state === "media") && state._cssPrettyPrint.lineLength > 60) {
346                     state._cssPrettyPrint.lineLength = 0;
347                     return 1;
348                 }
349                 return 0;
350             }
351             if (content === "{")
352                 return 1;
353             if (content === "}") // 2 newlines between rule declarations.
354                 return 2;
355             return 0;
356         }
357
358         if (isComment)
359             return 1;
360
361         return 0;
362     },
363
364     removeLastNewline: function(lastToken, lastContent, token, state, content, isComment, firstTokenOnLine)
365     {
366         if (isComment) { // Comment after semicolon.
367             if (!firstTokenOnLine && lastContent === ";")
368                 return true;
369             return false;
370         }
371
372         return content === "}" && (lastContent === "{" || lastContent === "}"); // "{}" and "}\n}" when closing @media.
373     },
374
375     indentAfterToken: function(lastToken, lastContent, token, state, content, isComment)
376     {
377         return content === "{";
378     },
379
380     newlineBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
381     {
382         return content === "}" && (lastContent !== "{" && lastContent !== "}"); // "{}" and "}\n}" when closing @media.
383     },
384
385     indentBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
386     {
387         return false;
388     },
389
390     dedentsBeforeToken: function(lastToken, lastContent, token, state, content, isComment)
391     {
392         return content === "}" ? 1 : 0;
393     },
394
395     modifyStateForTokenPost: function(lastToken, lastContent, token, state, content, isComment)
396     {
397         if (!state._cssPrettyPrint)
398             state._cssPrettyPrint = {lineLength: 0};
399
400         // In order insert newlines in selector lists we need keep track of the length of the current line.
401         // This isn't exact line length, only the builder knows that, but it is good enough to get an idea.
402         // If we are at a top level, keep track of the current line length, otherwise we reset to 0.
403         if (state.state === "top" || state.state === "media")
404             state._cssPrettyPrint.lineLength += content.length;
405         else
406             state._cssPrettyPrint.lineLength = 0;
407     }
408 });