• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
30importScripts("../common/utilities.js");
31importScripts("../cm/headlesscodemirror.js");
32importScripts("../cm/css.js");
33importScripts("../cm/javascript.js");
34importScripts("../cm/xml.js");
35importScripts("../cm/htmlmixed.js");
36WebInspector = {};
37FormatterWorker = {
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 */
63var FormatterParameters;
64
65var 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 */
76FormatterWorker.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 */
99FormatterWorker._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 */
112FormatterWorker.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
203FormatterWorker.CSSParserStates = {
204    Initial: "Initial",
205    Selector: "Selector",
206    Style: "Style",
207    PropertyName: "PropertyName",
208    PropertyValue: "PropertyValue",
209    AtRule: "AtRule",
210};
211
212FormatterWorker.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 */
330FormatterWorker._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 */
353FormatterWorker._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 */
371FormatterWorker.HTMLFormatter = function(indentString)
372{
373    this._indentString = indentString;
374}
375
376FormatterWorker.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/**
483 * @return {!Object}
484 */
485function require()
486{
487    return parse;
488}
489
490/**
491 * @type {!{tokenizer}}
492 */
493var exports = { tokenizer: null };
494importScripts("../UglifyJS/parse-js.js");
495var parse = exports;
496
497importScripts("JavaScriptFormatter.js");
498importScripts("CSSFormatter.js");
499