2 * Copyright (C) 2011 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
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
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.
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.
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");
39 * @param {string} mimeType
40 * @return {function(string, function(string, ?string, number, number))}
42 createTokenizer: function(mimeType)
44 var mode = CodeMirror.getMode({indentUnit: 2}, mimeType);
45 var state = CodeMirror.startState(mode);
46 function tokenize(line, callback)
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;
61 * @typedef {{indentString: string, content: string, mimeType: string}}
63 var FormatterParameters;
65 var onmessage = function(event) {
66 var data = /** @type !{method: string, params: !FormatterParameters} */ (event.data);
70 FormatterWorker[data.method](data.params);
74 * @param {!FormatterParameters} params
76 FormatterWorker.format = function(params)
78 // Default to a 4-space indent.
79 var indentString = params.indentString || " ";
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);
89 result.mapping = { original: [0], formatted: [0] };
90 result.content = FormatterWorker._formatScript(params.content, result.mapping, 0, 0, indentString);
96 * @param {number} totalLength
97 * @param {number} chunkSize
99 FormatterWorker._chunkCount = function(totalLength, chunkSize)
101 if (totalLength <= chunkSize)
104 var remainder = totalLength % chunkSize;
105 var partialLength = totalLength - remainder;
106 return (partialLength / chunkSize) + (remainder ? 1 : 0);
110 * @param {!Object} params
112 FormatterWorker.javaScriptOutline = function(params)
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) {
131 tokenizer(line, processToken);
135 * @param {?string} tokenType
138 function isJavaScriptIdentifier(tokenType)
142 return tokenType.startsWith("variable") || tokenType.startsWith("property") || tokenType === "def";
146 * @param {string} tokenValue
147 * @param {?string} tokenType
148 * @param {number} column
149 * @param {number} newColumn
151 function processToken(tokenValue, tokenType, column, newColumn)
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;
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;
171 } else if (tokenValue === "." && isJavaScriptIdentifier(previousTokenType))
172 previousIdentifier += ".";
173 else if (tokenValue === "(" && addedFunction)
174 isReadingArguments = true;
175 if (isReadingArguments && tokenValue)
176 argumentsText += tokenValue;
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");
183 outlineChunk.push(currentFunction);
186 if (tokenValue.trim().length) {
187 // Skip whitespace tokens.
188 previousToken = tokenValue;
189 previousTokenType = tokenType;
191 processedChunkCharacters += newColumn - column;
193 if (processedChunkCharacters >= chunkSize) {
194 postMessage({ chunk: outlineChunk, total: chunkCount, index: currentChunk++ });
196 processedChunkCharacters = 0;
200 postMessage({ chunk: outlineChunk, total: chunkCount, index: chunkCount });
203 FormatterWorker.CSSParserStates = {
205 Selector: "Selector",
207 PropertyName: "PropertyName",
208 PropertyValue: "PropertyValue",
212 FormatterWorker.parseCSS = function(params)
214 var chunkSize = 100000; // characters per data chunk
215 var lines = params.content.split("\n");
217 var processedChunkCharacters = 0;
219 var state = FormatterWorker.CSSParserStates.Initial;
222 var UndefTokenType = {};
225 * @param {string} tokenValue
226 * @param {?string} tokenTypes
227 * @param {number} column
228 * @param {number} newColumn
230 function processToken(tokenValue, tokenTypes, column, newColumn)
232 var tokenType = tokenTypes ? tokenTypes.split(" ").keySet() : UndefTokenType;
234 case FormatterWorker.CSSParserStates.Initial:
235 if (tokenType["qualifier"] || tokenType["builtin"] || tokenType["tag"]) {
237 selectorText: tokenValue,
238 lineNumber: lineNumber,
242 state = FormatterWorker.CSSParserStates.Selector;
243 } else if (tokenType["def"]) {
246 lineNumber: lineNumber,
249 state = FormatterWorker.CSSParserStates.AtRule;
252 case FormatterWorker.CSSParserStates.Selector:
253 if (tokenValue === "{" && tokenType === UndefTokenType) {
254 rule.selectorText = rule.selectorText.trim();
255 state = FormatterWorker.CSSParserStates.Style;
257 rule.selectorText += tokenValue;
260 case FormatterWorker.CSSParserStates.AtRule:
261 if ((tokenValue === ";" || tokenValue === "{") && tokenType === UndefTokenType) {
262 rule.atRule = rule.atRule.trim();
264 state = FormatterWorker.CSSParserStates.Initial;
266 rule.atRule += tokenValue;
269 case FormatterWorker.CSSParserStates.Style:
270 if (tokenType["meta"] || tokenType["property"]) {
275 state = FormatterWorker.CSSParserStates.PropertyName;
276 } else if (tokenValue === "}" && tokenType === UndefTokenType) {
278 state = FormatterWorker.CSSParserStates.Initial;
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;
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);
298 state = FormatterWorker.CSSParserStates.Initial;
299 } else if (!tokenType["comment"]) {
300 property.value += tokenValue;
304 console.assert(false, "Unknown CSS parser state.");
306 processedChunkCharacters += newColumn - column;
307 if (processedChunkCharacters > chunkSize) {
308 postMessage({ chunk: rules, isLastChunk: false });
310 processedChunkCharacters = 0;
313 var tokenizer = FormatterWorker.createTokenizer("text/css");
315 for (lineNumber = 0; lineNumber < lines.length; ++lineNumber) {
316 var line = lines[lineNumber];
317 tokenizer(line, processToken);
319 postMessage({ chunk: rules, isLastChunk: true });
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
330 FormatterWorker._formatScript = function(content, mapping, offset, formattedOffset, indentString)
332 var formattedContent;
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);
338 formattedContent = builder.content();
340 formattedContent = content;
342 return formattedContent;
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
353 FormatterWorker._formatCSS = function(content, mapping, offset, formattedOffset, indentString)
355 var formattedContent;
357 var builder = new FormatterWorker.CSSFormattedContentBuilder(content, mapping, offset, formattedOffset, indentString);
358 var formatter = new FormatterWorker.CSSFormatter(content, builder);
360 formattedContent = builder.content();
362 formattedContent = content;
364 return formattedContent;
369 * @param {string} indentString
371 FormatterWorker.HTMLFormatter = function(indentString)
373 this._indentString = indentString;
376 FormatterWorker.HTMLFormatter.prototype = {
378 * @param {string} content
379 * @return {!{content: string, mapping: {original: !Array.<number>, formatted: !Array.<number>}}}
381 format: function(content)
384 this._content = content;
385 this._formattedContent = "";
386 this._mapping = { original: [0], formatted: [0] };
389 var scriptOpened = false;
390 var styleOpened = false;
391 var tokenizer = FormatterWorker.createTokenizer("text/html");
394 * @this {FormatterWorker.HTMLFormatter}
396 function processToken(tokenValue, tokenType, tokenStart, tokenEnd) {
397 if (tokenType !== "tag")
399 if (tokenValue.toLowerCase() === "<script") {
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") {
408 } else if (styleOpened && tokenValue === ">") {
410 this._styleStarted(tokenEnd);
411 } else if (tokenValue.toLowerCase() === "</style") {
412 this._styleEnded(tokenStart);
415 tokenizer(content, processToken.bind(this));
417 this._formattedContent += this._content.substring(this._position);
418 return { content: this._formattedContent, mapping: this._mapping };
422 * @param {number} cursor
424 _scriptStarted: function(cursor)
426 this._handleSubFormatterStart(cursor);
430 * @param {number} cursor
432 _scriptEnded: function(cursor)
434 this._handleSubFormatterEnd(FormatterWorker._formatScript, cursor);
438 * @param {number} cursor
440 _styleStarted: function(cursor)
442 this._handleSubFormatterStart(cursor);
446 * @param {number} cursor
448 _styleEnded: function(cursor)
450 this._handleSubFormatterEnd(FormatterWorker._formatCSS, cursor);
454 * @param {number} cursor
456 _handleSubFormatterStart: function(cursor)
458 this._formattedContent += this._content.substring(this._position, cursor);
459 this._formattedContent += "\n";
460 this._position = cursor;
464 * @param {function(string, !{formatted: !Array.<number>, original: !Array.<number>}, number, number, string)} formatFunction
465 * @param {number} cursor
467 _handleSubFormatterEnd: function(formatFunction, cursor)
469 if (cursor === this._position)
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);
477 this._formattedContent += formattedScriptContent;
478 this._position = cursor;
482 Array.prototype.keySet = function()
485 for (var i = 0; i < this.length; ++i)
486 keys[this[i]] = true;
499 * @type {!{tokenizer}}
501 var exports = { tokenizer: null };
502 importScripts("../UglifyJS/parse-js.js");
505 importScripts("JavaScriptFormatter.js");
506 importScripts("CSSFormatter.js");