/**
* This file Copyright (c) 2005-2008 Aptana, Inc. This program is
* dual-licensed under both the Aptana Public License and the GNU General
* Public license. You may elect to use one or the other of these licenses.
*
* This program is distributed in the hope that it will be useful, but
* AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or
* NONINFRINGEMENT. Redistribution, except as permitted by whichever of
* the GPL or APL you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or modify this
* program under the terms of the GNU General Public License,
* Version 3, as published by the Free Software Foundation. You should
* have received a copy of the GNU General Public License, Version 3 along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Aptana provides a special exception to allow redistribution of this file
* with certain other free and open source software ("FOSS") code and certain additional terms
* pursuant to Section 7 of the GPL. You may view the exception and these
* terms on the web at http://www.aptana.com/legal/gpl/.
*
* 2. For the Aptana Public License (APL), this program and the
* accompanying materials are made available under the terms of the APL
* v1.0 which accompanies this distribution, and is available at
* http://www.aptana.com/legal/apl/.
*
* You may view the GPL, Aptana's exception and additional terms, and the
* APL in the file titled license.html at the root of the corresponding
* plugin containing this source file.
*
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.ide.editor.css.formatting;
import java.text.ParseException;
import java.util.Map;
import org.eclipse.core.resources.IProject;
import com.aptana.ide.editor.css.lexing.CSSTokenTypes;
import com.aptana.ide.editor.css.parsing.CSSMimeType;
import com.aptana.ide.editor.css.parsing.CSSParseState;
import com.aptana.ide.editors.unified.BaseFormatter;
import com.aptana.ide.editors.unified.LanguageRegistry;
import com.aptana.ide.io.SourceWriter;
import com.aptana.ide.lexer.Lexeme;
import com.aptana.ide.lexer.LexemeList;
import com.aptana.ide.lexer.LexerException;
import com.aptana.ide.lexer.TokenCategories;
import com.aptana.ide.parsing.IParser;
/**
* @author Ingo Muschenetz
*/
public class CSSCodeFormatter extends BaseFormatter
{
/**
* format
*
* @param notFormatted
* @param isSelection
* @param options
* @param project
* @param separator
* @return formatted lineDelimeters
*/
public String format(String notFormatted, boolean isSelection, Map options, IProject project, String lineDelimeters)
{
CSSCodeFormatterOptions codeoptions = new CSSCodeFormatterOptions(options, project);
if (!codeoptions.doFormatting)
{
return notFormatted;
}
return doLexerBasedFormat(notFormatted, codeoptions, lineDelimeters);
}
/**
* doLexerBasedFormat
*
* @param notFormatted
* @param codeoptions
* @param lineDelimeters
* @return
*/
private String doLexerBasedFormat(String notFormatted, CSSCodeFormatterOptions codeoptions, String lineDelimeters)
{
IParser parser = LanguageRegistry.getParser(CSSMimeType.MimeType);
CSSParseState parseState = (CSSParseState) parser.createParseState(null);
parseState.setEditState(notFormatted, notFormatted, 0, 0);
try
{
parser.parse(parseState);
LexemeList ll = parseState.getLexemeList();
String indent = codeoptions.formatterTabChar;
if (indent.length() == 0)
{
indent = " "; //$NON-NLS-1$
}
if (indent.charAt(0) == ' ')
{
StringBuffer bf = new StringBuffer();
for (int a = 0; a < codeoptions.tabSize; a++)
{
bf.append(' ');
}
indent = bf.toString();
}
SourceWriter writer = new SourceWriter(0, indent, codeoptions.tabSize);
if (lineDelimeters != null)
{
writer.setLineDelimeter(lineDelimeters);
}
String formatted = format(notFormatted, writer, ll, codeoptions);
String start = getStartLineBreaks(writer, notFormatted, formatted);
String end = getEndLineBreaks(writer, notFormatted, formatted);
formatted = start + formatted + end;
if (!isFormattingCorrect(ll, parser, notFormatted, formatted, new int[] { CSSTokenTypes.COMMENT }, null))
{
formatted = notFormatted;
}
return formatted;
}
catch (LexerException e)
{
return null;
}
catch (ParseException e)
{
return null;
}
}
/**
* isOnSameLine
*
* @param source
* @param list
* @param first
* @param second
* @return
*/
private boolean isOnSameLine(String source, LexemeList list, int first, int second)
{
if (first < 0)
{
return true;
}
if (second >= list.size())
{
return true;
}
int endingOffset = list.get(first).getEndingOffset();
int startingOffset = list.get(second).getStartingOffset();
for (int a = endingOffset; a < startingOffset; a++)
{
char c = source.charAt(a);
if (c == '\r' || c == '\n')
{
return false;
}
}
return true;
}
/**
* hasSpaceBetween
*
* @param source
* @param list
* @param first
* @param second
* @return
*/
private boolean hasSpaceBetween(String source, LexemeList list, int first, int second)
{
int endingOffset = 0;
int startingOffset = 0;
if (first >= 0)
{
endingOffset = list.get(first).getEndingOffset();
}
if (second < list.size())
{
startingOffset = list.get(second).getStartingOffset();
}
for (int a = endingOffset; a < startingOffset; a++)
{
char c = source.charAt(a);
if (Character.isWhitespace(c))
{
if (c != '\r' && c != '\n')
{
return true;
}
}
}
return false;
}
/**
* format
*
* @param source
* @param writer
* @param ll
* @param codeoptions
* @return
*/
private String format(String source, SourceWriter writer, LexemeList ll, CSSCodeFormatterOptions codeoptions)
{
Lexeme previousLexeme = null;
boolean isInProperty = false;
boolean newLine = false;
// state variables for dealing with errors
boolean hasSpace = false;
boolean sameLine = true;
boolean lastError = true;
for (int i = 0; i < ll.size(); i++)
{
Lexeme lex = ll.get(i);
if (lex.getCategoryIndex() == TokenCategories.ERROR)
{
sameLine = isOnSameLine(source, ll, i - 1, i);
// preserving space and new line before error;
if (!sameLine)
{
if (writer.getCurrentIndentLevel() != 0)
{
writer.println();
writer.printIndent();
}
}
if (sameLine)
{
hasSpace = hasSpaceBetween(source, ll, i - 1, i);
if (hasSpace)
{
writer.print(' ');
}
}
// preserving space and new line after error;
// doing it here because multiple tokens may require this values.
boolean bsLine = isOnSameLine(source, ll, i, i + 1);
sameLine = bsLine;
hasSpace = hasSpaceBetween(source, ll, i, i + 1);
if (i < ll.size() - 1)
{
Lexeme nextLexeme = ll.get(i + 1);
bsLine |= isLexemeOfType(nextLexeme, CSSTokenTypes.RCURLY);
bsLine |= isLexemeOfType(nextLexeme, CSSTokenTypes.LCURLY);
}
if (!bsLine)
{
writer.println(lex.getText());
}
else
{
writer.print(lex.getText());
}
lastError = true;
// FIXME For now give up here and retain the source as is from this point forward! We shoudl be able to
// have the parser handle errors more gracefully in the future though (it resets to top-level rule
// regardless of context)
int start = lex.getEndingOffset();
writer.print(source.substring(start));
return writer.toString();
}
if (lastError)
{
lastError = false;
}
else
{
hasSpace = false;
sameLine = false;
}
switch (lex.typeIndex)
{
case CSSTokenTypes.LCURLY:
{
if (codeoptions.formatterBracePositionForBlock == CSSCodeFormatterOptions.NEXT_LINE)
{
writer.println();
writer.printIndent();
}
else if (codeoptions.formatterBracePositionForBlock == CSSCodeFormatterOptions.NEXT_LINE_SHIFTED)
{
writer.println();
writer.increaseIndent();
writer.printIndent();
}
else
{
writer.print(" ");//$NON-NLS-1$
}
writer.print(lex.getText());
writer.increaseIndent();
newLine = true;
break;
}
case CSSTokenTypes.COMMENT:
{
boolean addedLine = false;
if (previousLexeme != null)
{
if (isLexemeOfType(previousLexeme, CSSTokenTypes.RCURLY))
{
writer.println();
}
boolean split = splitByNewline(source, previousLexeme, lex);
if (split)
{
writer.println();
writer.printIndent();
addedLine = true;
}
}
if (previousLexeme != null && !addedLine)
{
writer.print(" "); //$NON-NLS-1$
printComment(writer, lex.getText());
}
else
{
printComment(writer, lex.getText());
}
break;
}
case CSSTokenTypes.RCURLY:
writer.println();
if (codeoptions.formatterBracePositionForBlock == CSSCodeFormatterOptions.NEXT_LINE_SHIFTED)
{
writer.decreaseIndent();
}
if (codeoptions.formatterBracePositionForBlock == CSSCodeFormatterOptions.NEXT_LINE_SHIFTED)
{
writer.printIndent();
}
writer.print(lex.getText());
writer.decreaseIndent();
newLine = true;
break;
case CSSTokenTypes.SEMICOLON:
writer.print(lex.getText());
newLine = true;
isInProperty = false;
break;
case CSSTokenTypes.AT_KEYWORD:
case CSSTokenTypes.SELECTOR:
{
// case of h1, h1 + h2 {
boolean isCommaBefore = isLexemeOfType(previousLexeme, CSSTokenTypes.COMMA);
boolean isCommentBefore = isLexemeOfType(previousLexeme, CSSTokenTypes.COMMENT);
if (previousLexeme != null && !isCommaBefore && !isCommentBefore)
{
if (!sameLine && isLexemeOfType(previousLexeme, CSSTokenTypes.RCURLY))
{
// if previous error was on same line does not print lines
writer.println();
writer.println();
}
else if (isLexemeOfType(previousLexeme, CSSTokenTypes.SEMICOLON))
{
lex.typeIndex = CSSTokenTypes.PROPERTY;
writer.println();
writer.printIndent();
}
}
if (isCommentBefore)
{
writer.println();
}
else if (isCommaBefore && !codeoptions.newlinesBetweenSelectors)
{
writer.print(" "); //$NON-NLS-1$
}
// checking for space
if (hasSpace)
{
writer.print(" "); //$NON-NLS-1$
}
writer.print(lex.getText());
sameLine = false;
hasSpace = false;
break;
}
case CSSTokenTypes.COMMA:
{
writer.print(lex.getText());
if (codeoptions.newlinesBetweenSelectors
&& (isLexemeOfType(previousLexeme, CSSTokenTypes.CLASS)
|| isLexemeOfType(previousLexeme, CSSTokenTypes.SELECTOR)
|| isLexemeOfType(previousLexeme, CSSTokenTypes.IDENTIFIER) || isLexemeOfType(
previousLexeme, CSSTokenTypes.HASH)))
{
writer.println();
}
break;
}
case CSSTokenTypes.LBRACKET:
case CSSTokenTypes.RBRACKET:
{
writer.print(lex.getText());
break;
}
case CSSTokenTypes.HASH:
case CSSTokenTypes.CLASS:
{
boolean isCommaBefore = isLexemeOfType(previousLexeme, CSSTokenTypes.COMMA);
boolean isCommentBefore = isLexemeOfType(previousLexeme, CSSTokenTypes.COMMENT);
if (lexemeIsEndOfBlock(previousLexeme))
{
writer.println();
writer.println();
writer.print(lex.getText());
}
else
{
if (isCommentBefore)
{
writer.println();
}
else if (isCommaBefore
|| ((previousLexeme != null && lex != null) && previousLexeme.getEndingOffset() < lex
.getStartingOffset()))
{
if (!codeoptions.newlinesBetweenSelectors)
writer.print(" "); //$NON-NLS-1$
}
writer.print(lex.getText());
}
break;
}
case CSSTokenTypes.PLUS:
case CSSTokenTypes.GREATER:
case CSSTokenTypes.STAR:
case CSSTokenTypes.EQUAL:
{
if (lex.typeIndex == CSSTokenTypes.STAR
&& (previousLexeme == null || lexemeIsEndOfBlock(previousLexeme)))
{
if (newLine)
{
writer.println();
writer.printIndent();
newLine = false;
}
writer.print(lex.getText());
}
else
{
writer.print(" " + lex.getText()); //$NON-NLS-1$
}
break;
}
case CSSTokenTypes.COLON:
{
isInProperty = isLexemeOfType(previousLexeme, CSSTokenTypes.PROPERTY);
if (!isInProperty)
{
if (hasSpaceBetween(source, ll, i - 1, i))
writer.print(" ");
}
writer.print(lex.getText());
break;
}
case CSSTokenTypes.IDENTIFIER:
{
if (((isLexemeOfType(previousLexeme, CSSTokenTypes.COLON) || isLexemeOfType(previousLexeme,
CSSTokenTypes.LBRACKET)) && !isInProperty)
|| isLexemeOfType(previousLexeme, CSSTokenTypes.FUNCTION))
{
writer.print(lex.getText());
}
else
{
if (!(codeoptions.newlinesBetweenSelectors && isLexemeOfType(previousLexeme, CSSTokenTypes.COMMA)))
writer.print(" "); //$NON-NLS-1$
writer.print(lex.getText()); //$NON-NLS-1$
}
break;
}
case CSSTokenTypes.IMPORT:
{
if (newLine)
{
writer.println();
newLine = false;
}
writer.printWithIndent(lex.getText());
break;
}
case CSSTokenTypes.RPAREN:
writer.print(lex.getText());
break;
default:
{
if (newLine)
{
writer.println();
writer.printIndent();
newLine = false;
}
else if (previousLexeme != null && previousLexeme.typeIndex != CSSTokenTypes.FUNCTION)
{
writer.print(" "); //$NON-NLS-1$
}
writer.print(lex.getText());
break;
}
}
previousLexeme = lex;
}
return writer.toString();
}
private boolean lexemeIsEndOfBlock(Lexeme previousLexeme)
{
return isLexemeOfType(previousLexeme, CSSTokenTypes.RCURLY)
|| isLexemeOfType(previousLexeme, CSSTokenTypes.SEMICOLON);
}
/**
* Prints the comment across multiple lines
*
* @param writer
* @param text
*/
private void printComment(SourceWriter writer, String text)
{
String[] split = text.split("\r\n|\r|\n"); //$NON-NLS-1$
if (split.length == 1)
{
writer.print(split[0]);
}
else
{
writer.println(split[0].trim());
for (int i = 1; i < split.length - 1; i++)
{
String string = split[i];
writer.printlnWithIndent(" " + string.trim()); //$NON-NLS-1$
}
writer.printWithIndent(" " + split[split.length - 1].trim()); //$NON-NLS-1$
}
}
/**
* Are these two lexemes split by a carriage return?
*
* @param source
* @param previousLexeme
* @param lex
* @return - boolean if split by new line
*/
private boolean splitByNewline(String source, Lexeme previousLexeme, Lexeme lex)
{
String sourceText = source.substring(previousLexeme.getEndingOffset(), lex.getStartingOffset());
return (sourceText.indexOf("\r") >= 0 || sourceText.indexOf("\n") >= 0); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* @see com.aptana.ide.editors.unified.ICodeFormatter#createNestedMark()
*/
public String createNestedMark()
{
return ""; //$NON-NLS-1$
}
/**
* @see com.aptana.ide.editors.unified.ICodeFormatter#handlesNested()
*/
public boolean handlesNested()
{
return true;
}
}