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