Upstream version 7.35.144.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / ScriptFormatterWorker.js
1 /*
2  * Copyright (C) 2011 Google 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 are
6  * met:
7  *
8  *     * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *     * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *     * Neither the name of Google Inc. nor the names of its
15  * contributors may be used to endorse or promote products derived from
16  * this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30 importScripts("utilities.js");
31 importScripts("cm/headlesscodemirror.js");
32 importScripts("cm/css.js");
33 importScripts("cm/javascript.js");
34 importScripts("cm/xml.js");
35 importScripts("cm/htmlmixed.js");
36 WebInspector = {};
37 FormatterWorker = {
38     /**
39      * @param {string} mimeType
40      * @return {function(string, function(string, ?string, number, number))}
41      */
42     createTokenizer: function(mimeType)
43     {
44         var mode = CodeMirror.getMode({indentUnit: 2}, mimeType);
45         var state = CodeMirror.startState(mode);
46         function tokenize(line, callback)
47         {
48             var stream = new CodeMirror.StringStream(line);
49             while (!stream.eol()) {
50                 var style = mode.token(stream, state);
51                 var value = stream.current();
52                 callback(value, style, stream.start, stream.start + value.length);
53                 stream.start = stream.pos;
54             }
55         }
56         return tokenize;
57     }
58 };
59
60 /**
61  * @typedef {{indentString: string, content: string, mimeType: string}}
62  */
63 var FormatterParameters;
64
65 var onmessage = function(event) {
66     var data = /** @type !{method: string, params: !FormatterParameters} */ (event.data);
67     if (!data.method)
68         return;
69
70     FormatterWorker[data.method](data.params);
71 };
72
73 /**
74  * @param {!FormatterParameters} params
75  */
76 FormatterWorker.format = function(params)
77 {
78     // Default to a 4-space indent.
79     var indentString = params.indentString || "    ";
80     var result = {};
81
82     if (params.mimeType === "text/html") {
83         var formatter = new FormatterWorker.HTMLFormatter(indentString);
84         result = formatter.format(params.content);
85     } else if (params.mimeType === "text/css") {
86         result.mapping = { original: [0], formatted: [0] };
87         result.content = FormatterWorker._formatCSS(params.content, result.mapping, 0, 0, indentString);
88     } else {
89         result.mapping = { original: [0], formatted: [0] };
90         result.content = FormatterWorker._formatScript(params.content, result.mapping, 0, 0, indentString);
91     }
92     postMessage(result);
93 }
94
95 /**
96  * @param {number} totalLength
97  * @param {number} chunkSize
98  */
99 FormatterWorker._chunkCount = function(totalLength, chunkSize)
100 {
101     if (totalLength <= chunkSize)
102         return 1;
103
104     var remainder = totalLength % chunkSize;
105     var partialLength = totalLength - remainder;
106     return (partialLength / chunkSize) + (remainder ? 1 : 0);
107 }
108
109 /**
110  * @param {!Object} params
111  */
112 FormatterWorker.javaScriptOutline = function(params)
113 {
114     var chunkSize = 100000; // characters per data chunk
115     var totalLength = params.content.length;
116     var lines = params.content.split("\n");
117     var chunkCount = FormatterWorker._chunkCount(totalLength, chunkSize);
118     var outlineChunk = [];
119     var previousIdentifier = null;
120     var previousToken = null;
121     var previousTokenType = null;
122     var currentChunk = 1;
123     var processedChunkCharacters = 0;
124     var addedFunction = false;
125     var isReadingArguments = false;
126     var argumentsText = "";
127     var currentFunction = null;
128     var tokenizer = FormatterWorker.createTokenizer("text/javascript");
129     for (var i = 0; i < lines.length; ++i) {
130         var line = lines[i];
131         tokenizer(line, processToken);
132     }
133
134     /**
135      * @param {?string} tokenType
136      * @return {boolean}
137      */
138     function isJavaScriptIdentifier(tokenType)
139     {
140         if (!tokenType)
141             return false;
142         return tokenType.startsWith("variable") || tokenType.startsWith("property") || tokenType === "def";
143     }
144
145     /**
146      * @param {string} tokenValue
147      * @param {?string} tokenType
148      * @param {number} column
149      * @param {number} newColumn
150      */
151     function processToken(tokenValue, tokenType, column, newColumn)
152     {
153         if (isJavaScriptIdentifier(tokenType)) {
154             previousIdentifier = tokenValue;
155             if (tokenValue && previousToken === "function") {
156                 // A named function: "function f...".
157                 currentFunction = { line: i, column: column, name: tokenValue };
158                 addedFunction = true;
159                 previousIdentifier = null;
160             }
161         } else if (tokenType === "keyword") {
162             if (tokenValue === "function") {
163                 if (previousIdentifier && (previousToken === "=" || previousToken === ":")) {
164                     // Anonymous function assigned to an identifier: "...f = function..."
165                     // or "funcName: function...".
166                     currentFunction = { line: i, column: column, name: previousIdentifier };
167                     addedFunction = true;
168                     previousIdentifier = null;
169                 }
170             }
171         } else if (tokenValue === "." && isJavaScriptIdentifier(previousTokenType))
172             previousIdentifier += ".";
173         else if (tokenValue === "(" && addedFunction)
174             isReadingArguments = true;
175         if (isReadingArguments && tokenValue)
176             argumentsText += tokenValue;
177
178         if (tokenValue === ")" && isReadingArguments) {
179             addedFunction = false;
180             isReadingArguments = false;
181             currentFunction.arguments = argumentsText.replace(/,[\r\n\s]*/g, ", ").replace(/([^,])[\r\n\s]+/g, "$1");
182             argumentsText = "";
183             outlineChunk.push(currentFunction);
184         }
185
186         if (tokenValue.trim().length) {
187             // Skip whitespace tokens.
188             previousToken = tokenValue;
189             previousTokenType = tokenType;
190         }
191         processedChunkCharacters += newColumn - column;
192
193         if (processedChunkCharacters >= chunkSize) {
194             postMessage({ chunk: outlineChunk, total: chunkCount, index: currentChunk++ });
195             outlineChunk = [];
196             processedChunkCharacters = 0;
197         }
198     }
199
200     postMessage({ chunk: outlineChunk, total: chunkCount, index: chunkCount });
201 }
202
203 FormatterWorker.CSSParserStates = {
204     Initial: "Initial",
205     Selector: "Selector",
206     Style: "Style",
207     PropertyName: "PropertyName",
208     PropertyValue: "PropertyValue",
209     AtRule: "AtRule",
210 };
211
212 FormatterWorker.parseCSS = function(params)
213 {
214     var chunkSize = 100000; // characters per data chunk
215     var lines = params.content.split("\n");
216     var rules = [];
217     var processedChunkCharacters = 0;
218
219     var state = FormatterWorker.CSSParserStates.Initial;
220     var rule;
221     var property;
222     var UndefTokenType = {};
223
224     /**
225      * @param {string} tokenValue
226      * @param {?string} tokenTypes
227      * @param {number} column
228      * @param {number} newColumn
229      */
230     function processToken(tokenValue, tokenTypes, column, newColumn)
231     {
232         var tokenType = tokenTypes ? tokenTypes.split(" ").keySet() : UndefTokenType;
233         switch (state) {
234         case FormatterWorker.CSSParserStates.Initial:
235             if (tokenType["qualifier"] || tokenType["builtin"] || tokenType["tag"]) {
236                 rule = {
237                     selectorText: tokenValue,
238                     lineNumber: lineNumber,
239                     columNumber: column,
240                     properties: [],
241                 };
242                 state = FormatterWorker.CSSParserStates.Selector;
243             } else if (tokenType["def"]) {
244                 rule = {
245                     atRule: tokenValue,
246                     lineNumber: lineNumber,
247                     columNumber: column,
248                 };
249                 state = FormatterWorker.CSSParserStates.AtRule;
250             }
251             break;
252         case FormatterWorker.CSSParserStates.Selector:
253             if (tokenValue === "{" && tokenType === UndefTokenType) {
254                 rule.selectorText = rule.selectorText.trim();
255                 state = FormatterWorker.CSSParserStates.Style;
256             } else {
257                 rule.selectorText += tokenValue;
258             }
259             break;
260         case FormatterWorker.CSSParserStates.AtRule:
261             if ((tokenValue === ";" || tokenValue === "{") && tokenType === UndefTokenType) {
262                 rule.atRule = rule.atRule.trim();
263                 rules.push(rule);
264                 state = FormatterWorker.CSSParserStates.Initial;
265             } else {
266                 rule.atRule += tokenValue;
267             }
268             break;
269         case FormatterWorker.CSSParserStates.Style:
270             if (tokenType["meta"] || tokenType["property"]) {
271                 property = {
272                     name: tokenValue,
273                     value: "",
274                 };
275                 state = FormatterWorker.CSSParserStates.PropertyName;
276             } else if (tokenValue === "}" && tokenType === UndefTokenType) {
277                 rules.push(rule);
278                 state = FormatterWorker.CSSParserStates.Initial;
279             }
280             break;
281         case FormatterWorker.CSSParserStates.PropertyName:
282             if (tokenValue === ":" && tokenType["operator"]) {
283                 property.name = property.name.trim();
284                 state = FormatterWorker.CSSParserStates.PropertyValue;
285             } else if (tokenType["property"]) {
286                 property.name += tokenValue;
287             }
288             break;
289         case FormatterWorker.CSSParserStates.PropertyValue:
290             if (tokenValue === ";" && tokenType === UndefTokenType) {
291                 property.value = property.value.trim();
292                 rule.properties.push(property);
293                 state = FormatterWorker.CSSParserStates.Style;
294             } else if (tokenValue === "}" && tokenType === UndefTokenType) {
295                 property.value = property.value.trim();
296                 rule.properties.push(property);
297                 rules.push(rule);
298                 state = FormatterWorker.CSSParserStates.Initial;
299             } else if (!tokenType["comment"]) {
300                 property.value += tokenValue;
301             }
302             break;
303         default:
304             console.assert(false, "Unknown CSS parser state.");
305         }
306         processedChunkCharacters += newColumn - column;
307         if (processedChunkCharacters > chunkSize) {
308             postMessage({ chunk: rules, isLastChunk: false });
309             rules = [];
310             processedChunkCharacters = 0;
311         }
312     }
313     var tokenizer = FormatterWorker.createTokenizer("text/css");
314     var lineNumber;
315     for (lineNumber = 0; lineNumber < lines.length; ++lineNumber) {
316         var line = lines[lineNumber];
317         tokenizer(line, processToken);
318     }
319     postMessage({ chunk: rules, isLastChunk: true });
320 }
321
322 /**
323  * @param {string} content
324  * @param {!{original: !Array.<number>, formatted: !Array.<number>}} mapping
325  * @param {number} offset
326  * @param {number} formattedOffset
327  * @param {string} indentString
328  * @return {string}
329  */
330 FormatterWorker._formatScript = function(content, mapping, offset, formattedOffset, indentString)
331 {
332     var formattedContent;
333     try {
334         var tokenizer = new FormatterWorker.JavaScriptTokenizer(content);
335         var builder = new FormatterWorker.JavaScriptFormattedContentBuilder(tokenizer.content(), mapping, offset, formattedOffset, indentString);
336         var formatter = new FormatterWorker.JavaScriptFormatter(tokenizer, builder);
337         formatter.format();
338         formattedContent = builder.content();
339     } catch (e) {
340         formattedContent = content;
341     }
342     return formattedContent;
343 }
344
345 /**
346  * @param {string} content
347  * @param {!{original: !Array.<number>, formatted: !Array.<number>}} mapping
348  * @param {number} offset
349  * @param {number} formattedOffset
350  * @param {string} indentString
351  * @return {string}
352  */
353 FormatterWorker._formatCSS = function(content, mapping, offset, formattedOffset, indentString)
354 {
355     var formattedContent;
356     try {
357         var builder = new FormatterWorker.CSSFormattedContentBuilder(content, mapping, offset, formattedOffset, indentString);
358         var formatter = new FormatterWorker.CSSFormatter(content, builder);
359         formatter.format();
360         formattedContent = builder.content();
361     } catch (e) {
362         formattedContent = content;
363     }
364     return formattedContent;
365 }
366
367 /**
368  * @constructor
369  * @param {string} indentString
370  */
371 FormatterWorker.HTMLFormatter = function(indentString)
372 {
373     this._indentString = indentString;
374 }
375
376 FormatterWorker.HTMLFormatter.prototype = {
377     /**
378      * @param {string} content
379      * @return {!{content: string, mapping: {original: !Array.<number>, formatted: !Array.<number>}}}
380      */
381     format: function(content)
382     {
383         this.line = content;
384         this._content = content;
385         this._formattedContent = "";
386         this._mapping = { original: [0], formatted: [0] };
387         this._position = 0;
388
389         var scriptOpened = false;
390         var styleOpened = false;
391         var tokenizer = FormatterWorker.createTokenizer("text/html");
392
393         /**
394          * @this {FormatterWorker.HTMLFormatter}
395          */
396         function processToken(tokenValue, tokenType, tokenStart, tokenEnd) {
397             if (tokenType !== "tag")
398                 return;
399             if (tokenValue.toLowerCase() === "<script") {
400                 scriptOpened = true;
401             } else if (scriptOpened && tokenValue === ">") {
402                 scriptOpened = false;
403                 this._scriptStarted(tokenEnd);
404             } else if (tokenValue.toLowerCase() === "</script") {
405                 this._scriptEnded(tokenStart);
406             } else if (tokenValue.toLowerCase() === "<style") {
407                 styleOpened = true;
408             } else if (styleOpened && tokenValue === ">") {
409                 styleOpened = false;
410                 this._styleStarted(tokenEnd);
411             } else if (tokenValue.toLowerCase() === "</style") {
412                 this._styleEnded(tokenStart);
413             }
414         }
415         tokenizer(content, processToken.bind(this));
416
417         this._formattedContent += this._content.substring(this._position);
418         return { content: this._formattedContent, mapping: this._mapping };
419     },
420
421     /**
422      * @param {number} cursor
423      */
424     _scriptStarted: function(cursor)
425     {
426         this._handleSubFormatterStart(cursor);
427     },
428
429     /**
430      * @param {number} cursor
431      */
432     _scriptEnded: function(cursor)
433     {
434         this._handleSubFormatterEnd(FormatterWorker._formatScript, cursor);
435     },
436
437     /**
438      * @param {number} cursor
439      */
440     _styleStarted: function(cursor)
441     {
442         this._handleSubFormatterStart(cursor);
443     },
444
445     /**
446      * @param {number} cursor
447      */
448     _styleEnded: function(cursor)
449     {
450         this._handleSubFormatterEnd(FormatterWorker._formatCSS, cursor);
451     },
452
453     /**
454      * @param {number} cursor
455      */
456     _handleSubFormatterStart: function(cursor)
457     {
458         this._formattedContent += this._content.substring(this._position, cursor);
459         this._formattedContent += "\n";
460         this._position = cursor;
461     },
462
463     /**
464      * @param {function(string, !{formatted: !Array.<number>, original: !Array.<number>}, number, number, string)} formatFunction
465      * @param {number} cursor
466      */
467     _handleSubFormatterEnd: function(formatFunction, cursor)
468     {
469         if (cursor === this._position)
470             return;
471
472         var scriptContent = this._content.substring(this._position, cursor);
473         this._mapping.original.push(this._position);
474         this._mapping.formatted.push(this._formattedContent.length);
475         var formattedScriptContent = formatFunction(scriptContent, this._mapping, this._position, this._formattedContent.length, this._indentString);
476
477         this._formattedContent += formattedScriptContent;
478         this._position = cursor;
479     }
480 }
481
482 Array.prototype.keySet = function()
483 {
484     var keys = {};
485     for (var i = 0; i < this.length; ++i)
486         keys[this[i]] = true;
487     return keys;
488 };
489
490 /**
491  * @return {!Object}
492  */
493 function require()
494 {
495     return parse;
496 }
497
498 /**
499  * @type {!{tokenizer}}
500  */
501 var exports = { tokenizer: null };
502 importScripts("UglifyJS/parse-js.js");
503 var parse = exports;
504
505 importScripts("JavaScriptFormatter.js");
506 importScripts("CSSFormatter.js");