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.
32 * @param {string} mimeType
33 * @return {function(string, function(string, ?string, number, number))}
35 createTokenizer: function(mimeType)
37 var mode = CodeMirror.getMode({indentUnit: 2}, mimeType);
38 var state = CodeMirror.startState(mode);
39 function tokenize(line, callback)
41 var stream = new CodeMirror.StringStream(line);
42 while (!stream.eol()) {
43 var style = mode.token(stream, state);
44 var value = stream.current();
45 callback(value, style, stream.start, stream.start + value.length);
46 stream.start = stream.pos;
54 * @typedef {{indentString: string, content: string, mimeType: string}}
56 var FormatterParameters;
58 var onmessage = function(event) {
59 var data = /** @type !{method: string, params: !FormatterParameters} */ (event.data);
63 FormatterWorker[data.method](data.params);
67 * @param {!FormatterParameters} params
69 FormatterWorker.format = function(params)
71 // Default to a 4-space indent.
72 var indentString = params.indentString || " ";
75 if (params.mimeType === "text/html") {
76 var formatter = new FormatterWorker.HTMLFormatter(indentString);
77 result = formatter.format(params.content);
78 } else if (params.mimeType === "text/css") {
79 result.mapping = { original: [0], formatted: [0] };
80 result.content = FormatterWorker._formatCSS(params.content, result.mapping, 0, 0, indentString);
82 result.mapping = { original: [0], formatted: [0] };
83 result.content = FormatterWorker._formatScript(params.content, result.mapping, 0, 0, indentString);
89 * @param {number} totalLength
90 * @param {number} chunkSize
92 FormatterWorker._chunkCount = function(totalLength, chunkSize)
94 if (totalLength <= chunkSize)
97 var remainder = totalLength % chunkSize;
98 var partialLength = totalLength - remainder;
99 return (partialLength / chunkSize) + (remainder ? 1 : 0);
103 * @param {!Object} params
105 FormatterWorker.javaScriptOutline = function(params)
107 var chunkSize = 100000; // characters per data chunk
108 var totalLength = params.content.length;
109 var lines = params.content.split("\n");
110 var chunkCount = FormatterWorker._chunkCount(totalLength, chunkSize);
111 var outlineChunk = [];
112 var previousIdentifier = null;
113 var previousToken = null;
114 var previousTokenType = null;
115 var currentChunk = 1;
116 var processedChunkCharacters = 0;
117 var addedFunction = false;
118 var isReadingArguments = false;
119 var argumentsText = "";
120 var currentFunction = null;
121 var tokenizer = FormatterWorker.createTokenizer("text/javascript");
122 for (var i = 0; i < lines.length; ++i) {
124 tokenizer(line, processToken);
128 * @param {?string} tokenType
131 function isJavaScriptIdentifier(tokenType)
135 return tokenType.startsWith("variable") || tokenType.startsWith("property") || tokenType === "def";
139 * @param {string} tokenValue
140 * @param {?string} tokenType
141 * @param {number} column
142 * @param {number} newColumn
144 function processToken(tokenValue, tokenType, column, newColumn)
146 if (tokenType === "property" && previousTokenType === "property" && (previousToken === "get" || previousToken === "set")) {
147 currentFunction = { line: i, column: column, name: previousToken + " " + tokenValue };
148 addedFunction = true;
149 previousIdentifier = null;
150 } else if (isJavaScriptIdentifier(tokenType)) {
151 previousIdentifier = tokenValue;
152 if (tokenValue && previousToken === "function") {
153 // A named function: "function f...".
154 currentFunction = { line: i, column: column, name: tokenValue };
155 addedFunction = true;
156 previousIdentifier = null;
158 } else if (tokenType === "keyword") {
159 if (tokenValue === "function") {
160 if (previousIdentifier && (previousToken === "=" || previousToken === ":")) {
161 // Anonymous function assigned to an identifier: "...f = function..."
162 // or "funcName: function...".
163 currentFunction = { line: i, column: column, name: previousIdentifier };
164 addedFunction = true;
165 previousIdentifier = null;
168 } else if (tokenValue === "." && isJavaScriptIdentifier(previousTokenType))
169 previousIdentifier += ".";
170 else if (tokenValue === "(" && addedFunction)
171 isReadingArguments = true;
172 if (isReadingArguments && tokenValue)
173 argumentsText += tokenValue;
175 if (tokenValue === ")" && isReadingArguments) {
176 addedFunction = false;
177 isReadingArguments = false;
178 currentFunction.arguments = argumentsText.replace(/,[\r\n\s]*/g, ", ").replace(/([^,])[\r\n\s]+/g, "$1");
180 outlineChunk.push(currentFunction);
183 if (tokenValue.trim().length) {
184 // Skip whitespace tokens.
185 previousToken = tokenValue;
186 previousTokenType = tokenType;
188 processedChunkCharacters += newColumn - column;
190 if (processedChunkCharacters >= chunkSize) {
191 postMessage({ chunk: outlineChunk, total: chunkCount, index: currentChunk++ });
193 processedChunkCharacters = 0;
197 postMessage({ chunk: outlineChunk, total: chunkCount, index: chunkCount });
200 FormatterWorker.CSSParserStates = {
202 Selector: "Selector",
204 PropertyName: "PropertyName",
205 PropertyValue: "PropertyValue",
209 FormatterWorker.parseCSS = function(params)
211 var chunkSize = 100000; // characters per data chunk
212 var lines = params.content.split("\n");
214 var processedChunkCharacters = 0;
216 var state = FormatterWorker.CSSParserStates.Initial;
219 var UndefTokenType = {};
222 * @param {string} tokenValue
223 * @param {?string} tokenTypes
224 * @param {number} column
225 * @param {number} newColumn
227 function processToken(tokenValue, tokenTypes, column, newColumn)
229 var tokenType = tokenTypes ? tokenTypes.split(" ").keySet() : UndefTokenType;
231 case FormatterWorker.CSSParserStates.Initial:
232 if (tokenType["qualifier"] || tokenType["builtin"] || tokenType["tag"]) {
234 selectorText: tokenValue,
235 lineNumber: lineNumber,
239 state = FormatterWorker.CSSParserStates.Selector;
240 } else if (tokenType["def"]) {
243 lineNumber: lineNumber,
246 state = FormatterWorker.CSSParserStates.AtRule;
249 case FormatterWorker.CSSParserStates.Selector:
250 if (tokenValue === "{" && tokenType === UndefTokenType) {
251 rule.selectorText = rule.selectorText.trim();
252 state = FormatterWorker.CSSParserStates.Style;
254 rule.selectorText += tokenValue;
257 case FormatterWorker.CSSParserStates.AtRule:
258 if ((tokenValue === ";" || tokenValue === "{") && tokenType === UndefTokenType) {
259 rule.atRule = rule.atRule.trim();
261 state = FormatterWorker.CSSParserStates.Initial;
263 rule.atRule += tokenValue;
266 case FormatterWorker.CSSParserStates.Style:
267 if (tokenType["meta"] || tokenType["property"]) {
272 state = FormatterWorker.CSSParserStates.PropertyName;
273 } else if (tokenValue === "}" && tokenType === UndefTokenType) {
275 state = FormatterWorker.CSSParserStates.Initial;
278 case FormatterWorker.CSSParserStates.PropertyName:
279 if (tokenValue === ":" && tokenType === UndefTokenType) {
280 property.name = property.name.trim();
281 state = FormatterWorker.CSSParserStates.PropertyValue;
282 } else if (tokenType["property"]) {
283 property.name += tokenValue;
286 case FormatterWorker.CSSParserStates.PropertyValue:
287 if (tokenValue === ";" && tokenType === UndefTokenType) {
288 property.value = property.value.trim();
289 rule.properties.push(property);
290 state = FormatterWorker.CSSParserStates.Style;
291 } else if (tokenValue === "}" && tokenType === UndefTokenType) {
292 property.value = property.value.trim();
293 rule.properties.push(property);
295 state = FormatterWorker.CSSParserStates.Initial;
296 } else if (!tokenType["comment"]) {
297 property.value += tokenValue;
301 console.assert(false, "Unknown CSS parser state.");
303 processedChunkCharacters += newColumn - column;
304 if (processedChunkCharacters > chunkSize) {
305 postMessage({ chunk: rules, isLastChunk: false });
307 processedChunkCharacters = 0;
310 var tokenizer = FormatterWorker.createTokenizer("text/css");
312 for (lineNumber = 0; lineNumber < lines.length; ++lineNumber) {
313 var line = lines[lineNumber];
314 tokenizer(line, processToken);
316 postMessage({ chunk: rules, isLastChunk: true });
320 * @param {string} content
321 * @param {!{original: !Array.<number>, formatted: !Array.<number>}} mapping
322 * @param {number} offset
323 * @param {number} formattedOffset
324 * @param {string} indentString
327 FormatterWorker._formatScript = function(content, mapping, offset, formattedOffset, indentString)
329 var formattedContent;
331 var tokenizer = new FormatterWorker.JavaScriptTokenizer(content);
332 var builder = new FormatterWorker.JavaScriptFormattedContentBuilder(tokenizer.content(), mapping, offset, formattedOffset, indentString);
333 var formatter = new FormatterWorker.JavaScriptFormatter(tokenizer, builder);
335 formattedContent = builder.content();
337 formattedContent = content;
339 return formattedContent;
343 * @param {string} content
344 * @param {!{original: !Array.<number>, formatted: !Array.<number>}} mapping
345 * @param {number} offset
346 * @param {number} formattedOffset
347 * @param {string} indentString
350 FormatterWorker._formatCSS = function(content, mapping, offset, formattedOffset, indentString)
352 var formattedContent;
354 var builder = new FormatterWorker.CSSFormattedContentBuilder(content, mapping, offset, formattedOffset, indentString);
355 var formatter = new FormatterWorker.CSSFormatter(content, builder);
357 formattedContent = builder.content();
359 formattedContent = content;
361 return formattedContent;
366 * @param {string} indentString
368 FormatterWorker.HTMLFormatter = function(indentString)
370 this._indentString = indentString;
373 FormatterWorker.HTMLFormatter.prototype = {
375 * @param {string} content
376 * @return {!{content: string, mapping: {original: !Array.<number>, formatted: !Array.<number>}}}
378 format: function(content)
381 this._content = content;
382 this._formattedContent = "";
383 this._mapping = { original: [0], formatted: [0] };
386 var scriptOpened = false;
387 var styleOpened = false;
388 var tokenizer = FormatterWorker.createTokenizer("text/html");
389 var accumulatedTokenValue = "";
390 var accumulatedTokenStart = 0;
393 * @this {FormatterWorker.HTMLFormatter}
395 function processToken(tokenValue, tokenType, tokenStart, tokenEnd) {
398 var oldType = tokenType;
399 tokenType = tokenType.split(" ").keySet();
400 if (!tokenType["tag"])
402 if (tokenType["bracket"] && (tokenValue === "<" || tokenValue === "</")) {
403 accumulatedTokenValue = tokenValue;
404 accumulatedTokenStart = tokenStart;
407 accumulatedTokenValue = accumulatedTokenValue + tokenValue.toLowerCase();
408 if (accumulatedTokenValue === "<script") {
410 } else if (scriptOpened && tokenValue === ">") {
411 scriptOpened = false;
412 this._scriptStarted(tokenEnd);
413 } else if (accumulatedTokenValue === "</script") {
414 this._scriptEnded(accumulatedTokenStart);
415 } else if (accumulatedTokenValue === "<style") {
417 } else if (styleOpened && tokenValue === ">") {
419 this._styleStarted(tokenEnd);
420 } else if (accumulatedTokenValue === "</style") {
421 this._styleEnded(accumulatedTokenStart);
423 accumulatedTokenValue = "";
425 tokenizer(content, processToken.bind(this));
427 this._formattedContent += this._content.substring(this._position);
428 return { content: this._formattedContent, mapping: this._mapping };
432 * @param {number} cursor
434 _scriptStarted: function(cursor)
436 this._handleSubFormatterStart(cursor);
440 * @param {number} cursor
442 _scriptEnded: function(cursor)
444 this._handleSubFormatterEnd(FormatterWorker._formatScript, cursor);
448 * @param {number} cursor
450 _styleStarted: function(cursor)
452 this._handleSubFormatterStart(cursor);
456 * @param {number} cursor
458 _styleEnded: function(cursor)
460 this._handleSubFormatterEnd(FormatterWorker._formatCSS, cursor);
464 * @param {number} cursor
466 _handleSubFormatterStart: function(cursor)
468 this._formattedContent += this._content.substring(this._position, cursor);
469 this._formattedContent += "\n";
470 this._position = cursor;
474 * @param {function(string, !{formatted: !Array.<number>, original: !Array.<number>}, number, number, string)} formatFunction
475 * @param {number} cursor
477 _handleSubFormatterEnd: function(formatFunction, cursor)
479 if (cursor === this._position)
482 var scriptContent = this._content.substring(this._position, cursor);
483 this._mapping.original.push(this._position);
484 this._mapping.formatted.push(this._formattedContent.length);
485 var formattedScriptContent = formatFunction(scriptContent, this._mapping, this._position, this._formattedContent.length, this._indentString);
487 this._formattedContent += formattedScriptContent;
488 this._position = cursor;
497 return tokenizerHolder;
501 * @type {!{tokenizer}}
503 var exports = { tokenizer: null };
504 var tokenizerHolder = exports;