Home | History | Annotate | Download | only in script_formatter_worker
      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("../common/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 /**
    483  * @return {!Object}
    484  */
    485 function require()
    486 {
    487     return parse;
    488 }
    489 
    490 /**
    491  * @type {!{tokenizer}}
    492  */
    493 var exports = { tokenizer: null };
    494 importScripts("../UglifyJS/parse-js.js");
    495 var parse = exports;
    496 
    497 importScripts("JavaScriptFormatter.js");
    498 importScripts("CSSFormatter.js");
    499