blob: eeb464974778dd434c229866824e175c5c54ddb3 [file] [log] [blame]
/*
* Copyright (C) 2011 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
var FormatterWorker = {
/**
* @param {string} mimeType
* @return {function(string, function(string, ?string, number, number))}
*/
createTokenizer: function(mimeType)
{
var mode = CodeMirror.getMode({indentUnit: 2}, mimeType);
var state = CodeMirror.startState(mode);
function tokenize(line, callback)
{
var stream = new CodeMirror.StringStream(line);
while (!stream.eol()) {
var style = mode.token(stream, state);
var value = stream.current();
callback(value, style, stream.start, stream.start + value.length);
stream.start = stream.pos;
}
}
return tokenize;
}
};
/**
* @typedef {{indentString: string, content: string, mimeType: string}}
*/
var FormatterParameters;
self.onmessage = function(event) {
var data = /** @type !{method: string, params: !FormatterParameters} */ (event.data);
if (!data.method)
return;
FormatterWorker[data.method](data.params);
};
/**
* @param {!FormatterParameters} params
*/
FormatterWorker.format = function(params)
{
// Default to a 4-space indent.
var indentString = params.indentString || " ";
var result = {};
if (params.mimeType === "text/html") {
var formatter = new FormatterWorker.HTMLFormatter(indentString);
result = formatter.format(params.content);
} else if (params.mimeType === "text/css") {
result.mapping = { original: [0], formatted: [0] };
result.content = FormatterWorker._formatCSS(params.content, result.mapping, 0, 0, indentString);
} else {
result.mapping = { original: [0], formatted: [0] };
result.content = FormatterWorker._formatScript(params.content, result.mapping, 0, 0, indentString);
}
postMessage(result);
}
/**
* @param {!Object} params
*/
FormatterWorker.javaScriptOutline = function(params)
{
var chunkSize = 100000; // characters per data chunk
var outlineChunk = [];
var previousIdentifier = null;
var previousToken = null;
var processedChunkCharacters = 0;
var addedFunction = false;
var isReadingArguments = false;
var argumentsText = "";
var currentFunction = null;
var tokenizer = new WebInspector.AcornTokenizer(params.content);
var AT = WebInspector.AcornTokenizer;
while (tokenizer.peekToken()) {
var token = /** @type {!Acorn.TokenOrComment} */(tokenizer.nextToken());
if (AT.lineComment(token) || AT.blockComment(token))
continue;
var tokenValue = params.content.substring(token.start, token.end);
if (AT.identifier(token) && previousToken && (AT.identifier(previousToken, "get") || AT.identifier(previousToken, "set"))) {
currentFunction = {
line: tokenizer.tokenLineStart(),
column: tokenizer.tokenColumnStart(),
name : previousToken.value + " " + tokenValue
};
addedFunction = true;
previousIdentifier = null;
} else if (AT.identifier(token)) {
previousIdentifier = tokenValue;
if (tokenValue && previousToken && AT.keyword(previousToken, "function")) {
// A named function: "function f...".
currentFunction = {
line: tokenizer.tokenLineStart(),
column: tokenizer.tokenColumnStart(),
name: tokenValue
};
addedFunction = true;
previousIdentifier = null;
}
} else if (AT.keyword(token, "function") && previousIdentifier && previousToken && AT.punctuator(previousToken, ":=")) {
// Anonymous function assigned to an identifier: "...f = function..."
// or "funcName: function...".
currentFunction = {
line: tokenizer.tokenLineStart(),
column: tokenizer.tokenColumnStart(),
name: previousIdentifier
};
addedFunction = true;
previousIdentifier = null;
} else if (AT.punctuator(token, ".") && previousToken && AT.identifier(previousToken))
previousIdentifier += ".";
else if (AT.punctuator(token, "(") && addedFunction)
isReadingArguments = true;
if (isReadingArguments && tokenValue)
argumentsText += tokenValue;
if (AT.punctuator(token, ")") && isReadingArguments) {
addedFunction = false;
isReadingArguments = false;
currentFunction.arguments = argumentsText.replace(/,[\r\n\s]*/g, ", ").replace(/([^,])[\r\n\s]+/g, "$1");
argumentsText = "";
outlineChunk.push(currentFunction);
}
previousToken = token;
processedChunkCharacters += token.end - token.start;
if (processedChunkCharacters >= chunkSize) {
postMessage({ chunk: outlineChunk, isLastChunk: false });
outlineChunk = [];
processedChunkCharacters = 0;
}
}
postMessage({ chunk: outlineChunk, isLastChunk: true });
}
FormatterWorker.CSSParserStates = {
Initial: "Initial",
Selector: "Selector",
Style: "Style",
PropertyName: "PropertyName",
PropertyValue: "PropertyValue",
AtRule: "AtRule"
};
FormatterWorker.parseCSS = function(params)
{
FormatterWorker._innerParseCSS(params.content, postMessage);
}
FormatterWorker._innerParseCSS = function(text, chunkCallback)
{
var chunkSize = 100000; // characters per data chunk
var lines = text.split("\n");
var rules = [];
var processedChunkCharacters = 0;
var state = FormatterWorker.CSSParserStates.Initial;
var rule;
var property;
var UndefTokenType = {};
var disabledRules = [];
function disabledRulesCallback(chunk)
{
disabledRules = disabledRules.concat(chunk.chunk);
}
/**
* @param {string} tokenValue
* @param {?string} tokenTypes
* @param {number} column
* @param {number} newColumn
*/
function processToken(tokenValue, tokenTypes, column, newColumn)
{
var tokenType = tokenTypes ? tokenTypes.split(" ").keySet() : UndefTokenType;
switch (state) {
case FormatterWorker.CSSParserStates.Initial:
if (tokenType["qualifier"] || tokenType["builtin"] || tokenType["tag"]) {
rule = {
selectorText: tokenValue,
lineNumber: lineNumber,
columnNumber: column,
properties: [],
};
state = FormatterWorker.CSSParserStates.Selector;
} else if (tokenType["def"]) {
rule = {
atRule: tokenValue,
lineNumber: lineNumber,
columnNumber: column,
};
state = FormatterWorker.CSSParserStates.AtRule;
}
break;
case FormatterWorker.CSSParserStates.Selector:
if (tokenValue === "{" && tokenType === UndefTokenType) {
rule.selectorText = rule.selectorText.trim();
rule.styleRange = createRange(lineNumber, newColumn);
state = FormatterWorker.CSSParserStates.Style;
} else {
rule.selectorText += tokenValue;
}
break;
case FormatterWorker.CSSParserStates.AtRule:
if ((tokenValue === ";" || tokenValue === "{") && tokenType === UndefTokenType) {
rule.atRule = rule.atRule.trim();
rules.push(rule);
state = FormatterWorker.CSSParserStates.Initial;
} else {
rule.atRule += tokenValue;
}
break;
case FormatterWorker.CSSParserStates.Style:
if (tokenType["meta"] || tokenType["property"]) {
property = {
name: tokenValue,
value: "",
range: createRange(lineNumber, column),
nameRange: createRange(lineNumber, column)
};
state = FormatterWorker.CSSParserStates.PropertyName;
} else if (tokenValue === "}" && tokenType === UndefTokenType) {
rule.styleRange.endLine = lineNumber;
rule.styleRange.endColumn = column;
rules.push(rule);
state = FormatterWorker.CSSParserStates.Initial;
} else if (tokenType["comment"]) {
// The |processToken| is called per-line, so no token spans more then one line.
// Support only a one-line comments.
if (tokenValue.substring(0, 2) !== "/*" || tokenValue.substring(tokenValue.length - 2) !== "*/")
break;
var uncommentedText = tokenValue.substring(2, tokenValue.length - 2);
var fakeRule = "a{\n" + uncommentedText + "}";
disabledRules = [];
FormatterWorker._innerParseCSS(fakeRule, disabledRulesCallback);
if (disabledRules.length === 1 && disabledRules[0].properties.length === 1) {
var disabledProperty = disabledRules[0].properties[0];
disabledProperty.disabled = true;
disabledProperty.range = createRange(lineNumber, column);
disabledProperty.range.endColumn = newColumn;
var lineOffset = lineNumber - 1;
var columnOffset = column + 2;
disabledProperty.nameRange.startLine += lineOffset;
disabledProperty.nameRange.startColumn += columnOffset;
disabledProperty.nameRange.endLine += lineOffset;
disabledProperty.nameRange.endColumn += columnOffset;
disabledProperty.valueRange.startLine += lineOffset;
disabledProperty.valueRange.startColumn += columnOffset;
disabledProperty.valueRange.endLine += lineOffset;
disabledProperty.valueRange.endColumn += columnOffset;
rule.properties.push(disabledProperty);
}
}
break;
case FormatterWorker.CSSParserStates.PropertyName:
if (tokenValue === ":" && tokenType === UndefTokenType) {
property.name = property.name;
property.nameRange.endLine = lineNumber;
property.nameRange.endColumn = column;
property.valueRange = createRange(lineNumber, newColumn);
state = FormatterWorker.CSSParserStates.PropertyValue;
} else if (tokenType["property"]) {
property.name += tokenValue;
}
break;
case FormatterWorker.CSSParserStates.PropertyValue:
if ((tokenValue === ";" || tokenValue === "}") && tokenType === UndefTokenType) {
property.value = property.value;
property.valueRange.endLine = lineNumber;
property.valueRange.endColumn = column;
property.range.endLine = lineNumber;
property.range.endColumn = tokenValue === ";" ? newColumn : column;
rule.properties.push(property);
if (tokenValue === "}") {
rule.styleRange.endLine = lineNumber;
rule.styleRange.endColumn = column;
rules.push(rule);
state = FormatterWorker.CSSParserStates.Initial;
} else {
state = FormatterWorker.CSSParserStates.Style;
}
} else if (!tokenType["comment"]) {
property.value += tokenValue;
}
break;
default:
console.assert(false, "Unknown CSS parser state.");
}
processedChunkCharacters += newColumn - column;
if (processedChunkCharacters > chunkSize) {
chunkCallback({ chunk: rules, isLastChunk: false });
rules = [];
processedChunkCharacters = 0;
}
}
var tokenizer = FormatterWorker.createTokenizer("text/css");
var lineNumber;
for (lineNumber = 0; lineNumber < lines.length; ++lineNumber) {
var line = lines[lineNumber];
tokenizer(line, processToken);
processToken("\n", null, line.length, line.length + 1);
}
chunkCallback({ chunk: rules, isLastChunk: true });
/**
* @return {!{startLine: number, startColumn: number, endLine: number, endColumn: number}}
*/
function createRange(lineNumber, columnNumber)
{
return {
startLine: lineNumber,
startColumn: columnNumber,
endLine: lineNumber,
endColumn: columnNumber
};
}
}
/**
* @param {string} content
* @param {!{original: !Array.<number>, formatted: !Array.<number>}} mapping
* @param {number} offset
* @param {number} formattedOffset
* @param {string} indentString
* @return {string}
*/
FormatterWorker._formatScript = function(content, mapping, offset, formattedOffset, indentString)
{
var formattedContent;
try {
var builder = new FormatterWorker.JavaScriptFormattedContentBuilder(content, mapping, offset, formattedOffset, indentString);
var formatter = new FormatterWorker.JavaScriptFormatter(content, builder);
formatter.format();
formattedContent = builder.content();
} catch (e) {
console.error(e);
formattedContent = content;
}
return formattedContent;
}
/**
* @param {string} content
* @param {!{original: !Array.<number>, formatted: !Array.<number>}} mapping
* @param {number} offset
* @param {number} formattedOffset
* @param {string} indentString
* @return {string}
*/
FormatterWorker._formatCSS = function(content, mapping, offset, formattedOffset, indentString)
{
var formattedContent;
try {
var builder = new FormatterWorker.CSSFormattedContentBuilder(content, mapping, offset, formattedOffset, indentString);
var formatter = new FormatterWorker.CSSFormatter(content, builder);
formatter.format();
formattedContent = builder.content();
} catch (e) {
formattedContent = content;
}
return formattedContent;
}
/**
* @constructor
* @param {string} indentString
*/
FormatterWorker.HTMLFormatter = function(indentString)
{
this._indentString = indentString;
}
FormatterWorker.HTMLFormatter.prototype = {
/**
* @param {string} content
* @return {!{content: string, mapping: {original: !Array.<number>, formatted: !Array.<number>}}}
*/
format: function(content)
{
this.line = content;
this._content = content;
this._formattedContent = "";
this._mapping = { original: [0], formatted: [0] };
this._position = 0;
var scriptOpened = false;
var styleOpened = false;
var tokenizer = FormatterWorker.createTokenizer("text/html");
var accumulatedTokenValue = "";
var accumulatedTokenStart = 0;
/**
* @this {FormatterWorker.HTMLFormatter}
*/
function processToken(tokenValue, tokenType, tokenStart, tokenEnd) {
if (!tokenType)
return;
tokenType = tokenType.split(" ").keySet();
if (!tokenType["tag"])
return;
if (tokenType["bracket"] && (tokenValue === "<" || tokenValue === "</")) {
accumulatedTokenValue = tokenValue;
accumulatedTokenStart = tokenStart;
return;
}
accumulatedTokenValue = accumulatedTokenValue + tokenValue.toLowerCase();
if (accumulatedTokenValue === "<script") {
scriptOpened = true;
} else if (scriptOpened && tokenValue === ">") {
scriptOpened = false;
this._scriptStarted(tokenEnd);
} else if (accumulatedTokenValue === "</script") {
this._scriptEnded(accumulatedTokenStart);
} else if (accumulatedTokenValue === "<style") {
styleOpened = true;
} else if (styleOpened && tokenValue === ">") {
styleOpened = false;
this._styleStarted(tokenEnd);
} else if (accumulatedTokenValue === "</style") {
this._styleEnded(accumulatedTokenStart);
}
accumulatedTokenValue = "";
}
tokenizer(content, processToken.bind(this));
this._formattedContent += this._content.substring(this._position);
return { content: this._formattedContent, mapping: this._mapping };
},
/**
* @param {number} cursor
*/
_scriptStarted: function(cursor)
{
this._handleSubFormatterStart(cursor);
},
/**
* @param {number} cursor
*/
_scriptEnded: function(cursor)
{
this._handleSubFormatterEnd(FormatterWorker._formatScript, cursor);
},
/**
* @param {number} cursor
*/
_styleStarted: function(cursor)
{
this._handleSubFormatterStart(cursor);
},
/**
* @param {number} cursor
*/
_styleEnded: function(cursor)
{
this._handleSubFormatterEnd(FormatterWorker._formatCSS, cursor);
},
/**
* @param {number} cursor
*/
_handleSubFormatterStart: function(cursor)
{
this._formattedContent += this._content.substring(this._position, cursor);
this._formattedContent += "\n";
this._position = cursor;
},
/**
* @param {function(string, !{formatted: !Array.<number>, original: !Array.<number>}, number, number, string)} formatFunction
* @param {number} cursor
*/
_handleSubFormatterEnd: function(formatFunction, cursor)
{
if (cursor === this._position)
return;
var scriptContent = this._content.substring(this._position, cursor);
this._mapping.original.push(this._position);
this._mapping.formatted.push(this._formattedContent.length);
var formattedScriptContent = formatFunction(scriptContent, this._mapping, this._position, this._formattedContent.length, this._indentString);
this._formattedContent += formattedScriptContent;
this._position = cursor;
}
}
// A dummy javascript mode which is used only by htmlmixed mode to advance
// stream until a </script> is found.
CodeMirror.defineMode("javascript", function(config, parserConfig) {
return {
token: function(stream, state)
{
return stream.next();
}
}
});