blob: 1855f2a7c432ee3d23a396047f4c4f5a78c4d047 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004 John-Mason P. Shackelford and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* John-Mason P. Shackelford - initial API and implementation
* IBM Corporation - bug fixes
*******************************************************************************/
package org.eclipse.ant.internal.ui.editor.formatter;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import org.eclipse.ant.internal.ui.model.AntUIPlugin;
import org.eclipse.ant.internal.ui.preferences.AntEditorPreferenceConstants;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.Assert;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
public class XmlDocumentFormatter {
private static class CommentReader extends TagReader {
private boolean complete = false;
protected void clear() {
this.complete = false;
}
public String getStartOfTag() {
return "<!--"; //$NON-NLS-1$
}
protected String readTag() throws IOException {
int intChar;
char c;
StringBuffer node = new StringBuffer();
while (!complete && (intChar = reader.read()) != -1) {
c = (char) intChar;
node.append(c);
if (c == '>' && node.toString().endsWith("-->")) { //$NON-NLS-1$
complete = true;
}
}
return node.toString();
}
}
private static class DoctypeDeclarationReader extends TagReader {
private boolean complete = false;
protected void clear() {
this.complete = false;
}
public String getStartOfTag() {
return "<!"; //$NON-NLS-1$
}
protected String readTag() throws IOException {
int intChar;
char c;
StringBuffer node = new StringBuffer();
while (!complete && (intChar = reader.read()) != -1) {
c = (char) intChar;
node.append(c);
if (c == '>') {
complete = true;
}
}
return node.toString();
}
}
private static class ProcessingInstructionReader extends TagReader {
private boolean complete = false;
protected void clear() {
this.complete = false;
}
public String getStartOfTag() {
return "<?"; //$NON-NLS-1$
}
protected String readTag() throws IOException {
int intChar;
char c;
StringBuffer node = new StringBuffer();
while (!complete && (intChar = reader.read()) != -1) {
c = (char) intChar;
node.append(c);
if (c == '>' && node.toString().endsWith("?>")) { //$NON-NLS-1$
complete = true;
}
}
return node.toString();
}
}
private static abstract class TagReader {
protected Reader reader;
private String tagText;
protected abstract void clear();
public int getPostTagDepthModifier() {
return 0;
}
public int getPreTagDepthModifier() {
return 0;
}
abstract public String getStartOfTag();
public String getTagText() {
return this.tagText;
}
public boolean isTextNode() {
return false;
}
protected abstract String readTag() throws IOException;
public boolean requiresInitialIndent() {
return true;
}
public void setReader(Reader reader) throws IOException {
this.reader = reader;
this.clear();
this.tagText = readTag();
}
public boolean startsOnNewline() {
return true;
}
}
private static class TagReaderFactory {
// Warning: the order of the Array is important!
private static TagReader[] tagReaders = new TagReader[]{new CommentReader(),
new DoctypeDeclarationReader(),
new ProcessingInstructionReader(),
new XmlElementReader()};
private static TagReader textNodeReader = new TextReader();
public static TagReader createTagReaderFor(Reader reader)
throws IOException {
char[] buf = new char[10];
reader.mark(10);
reader.read(buf, 0, 10);
reader.reset();
String startOfTag = String.valueOf(buf);
for (int i = 0; i < tagReaders.length; i++) {
if (startOfTag.startsWith(tagReaders[i].getStartOfTag())) {
tagReaders[i].setReader(reader);
return tagReaders[i];
}
}
// else
textNodeReader.setReader(reader);
return textNodeReader;
}
}
private static class TextReader extends TagReader {
private boolean complete;
private boolean isTextNode;
protected void clear() {
this.complete = false;
}
/* (non-Javadoc)
* @see org.eclipse.ant.internal.ui.editor.formatter.XmlDocumentFormatter.TagReader#getStartOfTag()
*/
public String getStartOfTag() {
return ""; //$NON-NLS-1$
}
/* (non-Javadoc)
* @see org.eclipse.ant.internal.ui.editor.formatter.XmlDocumentFormatter.TagReader#isTextNode()
*/
public boolean isTextNode() {
return this.isTextNode;
}
protected String readTag() throws IOException {
StringBuffer node = new StringBuffer();
while (!complete) {
reader.mark(1);
int intChar = reader.read();
if (intChar == -1) break;
char c = (char) intChar;
if (c == '<') {
reader.reset();
complete = true;
} else {
node.append(c);
}
}
// if this text node is just whitespace
// remove it, except for the newlines.
if (node.length() < 1) {
this.isTextNode = false;
} else if (node.toString().trim().length() == 0) {
String whitespace = node.toString();
node = new StringBuffer();
for (int i = 0; i < whitespace.length(); i++) {
char whitespaceCharacter = whitespace.charAt(i);
if (whitespaceCharacter == '\n')
node.append(whitespaceCharacter);
}
this.isTextNode = false;
} else {
this.isTextNode = true;
}
return node.toString();
}
/* (non-Javadoc)
* @see org.eclipse.ant.internal.ui.editor.formatter.XmlDocumentFormatter.TagReader#requiresInitialIndent()
*/
public boolean requiresInitialIndent() {
return false;
}
/* (non-Javadoc)
* @see org.eclipse.ant.internal.ui.editor.formatter.XmlDocumentFormatter.TagReader#startsOnNewline()
*/
public boolean startsOnNewline() {
return false;
}
}
private static class XmlElementReader extends TagReader {
private boolean complete = false;
protected void clear() {
this.complete = false;
}
public int getPostTagDepthModifier() {
if (getTagText().endsWith("/>") || getTagText().endsWith("/ >")) { //$NON-NLS-1$ //$NON-NLS-2$
return 0;
} else if (getTagText().startsWith("</")) { //$NON-NLS-1$
return 0;
} else {
return +1;
}
}
public int getPreTagDepthModifier() {
if (getTagText().startsWith("</")) { //$NON-NLS-1$
return -1;
}
return 0;
}
public String getStartOfTag() {
return "<"; //$NON-NLS-1$
}
protected String readTag() throws IOException {
StringBuffer node = new StringBuffer();
boolean insideQuote = false;
int intChar;
while (!complete && (intChar = reader.read()) != -1) {
char c = (char) intChar;
node.append(c);
// TODO logic incorrectly assumes that " is quote character
// when it could also be '
if (c == '"') {
insideQuote = !insideQuote;
}
if (c == '>' && !insideQuote) {
complete = true;
}
}
return node.toString();
}
}
private int depth;
private StringBuffer formattedXml;
private boolean lastNodeWasText;
public XmlDocumentFormatter() {
super();
depth= -1;
}
private void copyNode(Reader reader, StringBuffer out, FormattingPreferences prefs) throws IOException {
TagReader tag = TagReaderFactory.createTagReaderFor(reader);
depth = depth + tag.getPreTagDepthModifier();
if (!lastNodeWasText) {
if (tag.startsOnNewline() && !hasNewlineAlready(out)) {
out.append("\n"); //$NON-NLS-1$
}
if (tag.requiresInitialIndent()) {
out.append(indent(prefs.getCanonicalIndent()));
}
}
out.append(tag.getTagText());
depth = depth + tag.getPostTagDepthModifier();
lastNodeWasText = tag.isTextNode();
}
/**
* Returns the indent of the given string.
*
* @param line the text line
* @param tabWidth the width of the '\t' character.
*/
public static int computeIndent(String line, int tabWidth) {
int result= 0;
int blanks= 0;
int size= line.length();
for (int i= 0; i < size; i++) {
char c= line.charAt(i);
if (c == '\t') {
result++;
blanks= 0;
} else if (isIndentChar(c)) {
blanks++;
if (blanks == tabWidth) {
result++;
blanks= 0;
}
} else {
return result;
}
}
return result;
}
/**
* Indent char is a space char but not a line delimiters.
* <code>== Character.isWhitespace(ch) && ch != '\n' && ch != '\r'</code>
*/
public static boolean isIndentChar(char ch) {
return Character.isWhitespace(ch) && !isLineDelimiterChar(ch);
}
/**
* Line delimiter chars are '\n' and '\r'.
*/
public static boolean isLineDelimiterChar(char ch) {
return ch == '\n' || ch == '\r';
}
/**
* @return
*/
public String format(String documentText, FormattingPreferences prefs) {
Assert.isNotNull(documentText);
Assert.isNotNull(prefs);
Reader reader = new StringReader(documentText);
formattedXml = new StringBuffer();
if (depth == -1) {
depth = 0;
}
lastNodeWasText = false;
try {
while (true) {
reader.mark(1);
int intChar = reader.read();
reader.reset();
if (intChar != -1) {
copyNode(reader, formattedXml, prefs);
} else {
break;
}
}
reader.close();
} catch (IOException e) {
AntUIPlugin.log(e);
}
return formattedXml.toString();
}
private boolean hasNewlineAlready(StringBuffer out) {
return out.lastIndexOf("\n") == formattedXml.length() - 1 //$NON-NLS-1$
|| out.lastIndexOf("\r") == formattedXml.length() - 1; //$NON-NLS-1$
}
private String indent(String canonicalIndent) {
StringBuffer indent = new StringBuffer(30);
for (int i = 0; i < depth; i++) {
indent.append(canonicalIndent);
}
return indent.toString();
}
public void setInitialIndent(int indent) {
depth= indent;
}
/**
* Returns the indentation of the line at <code>offset</code> as a
* <code>StringBuffer</code>. If the offset is not valid, the empty string
* is returned.
*
* @param offset the offset in the document
* @return the indentation (leading whitespace) of the line in which
* <code>offset</code> is located
*/
public static StringBuffer getLeadingWhitespace(int offset, IDocument document) {
StringBuffer indent= new StringBuffer();
try {
IRegion line= document.getLineInformationOfOffset(offset);
int lineOffset= line.getOffset();
int nonWS= findEndOfWhiteSpace(document, lineOffset, lineOffset + line.getLength());
indent.append(document.get(lineOffset, nonWS - lineOffset));
return indent;
} catch (BadLocationException e) {
return indent;
}
}
/**
* Returns the first offset greater than <code>offset</code> and smaller than
* <code>end</code> whose character is not a space or tab character. If no such
* offset is found, <code>end</code> is returned.
*
* @param document the document to search in
* @param offset the offset at which searching start
* @param end the offset at which searching stops
* @return the offset in the specifed range whose character is not a space or tab
* @exception BadLocationException if position is an invalid range in the given document
*/
public static int findEndOfWhiteSpace(IDocument document, int offset, int end) throws BadLocationException {
while (offset < end) {
char c= document.getChar(offset);
if (c != ' ' && c != '\t') {
return offset;
}
offset++;
}
return end;
}
/**
* Creates a string that represents one indent (can be
* spaces or tabs..)
*
* @return one indentation
*/
public static StringBuffer createIndent() {
StringBuffer oneIndent= new StringBuffer();
IPreferenceStore pluginPrefs= AntUIPlugin.getDefault().getPreferenceStore();
pluginPrefs.getBoolean(AntEditorPreferenceConstants.FORMATTER_TAB_CHAR);
if (!pluginPrefs.getBoolean(AntEditorPreferenceConstants.FORMATTER_TAB_CHAR)) {
int tabLen= pluginPrefs.getInt(AntEditorPreferenceConstants.FORMATTER_TAB_SIZE);
for (int i= 0; i < tabLen; i++) {
oneIndent.append(' ');
}
} else {
oneIndent.append('\t'); // default
}
return oneIndent;
}
}