blob: 839bb5ecb18e33310b1763bd8d0268e674809508 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2011 John-Mason P. Shackelford and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* 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.core.IAntCoreConstants;
import org.eclipse.ant.internal.ui.AntUIPlugin;
import org.eclipse.ant.internal.ui.preferences.AntEditorPreferenceConstants;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.preference.IPreferenceStore;
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;
@Override
protected void clear() {
this.complete = false;
}
@Override
public String getStartOfTag() {
return "<!--"; //$NON-NLS-1$
}
@Override
protected String readTag() throws IOException {
int intChar;
char c;
StringBuilder node = new StringBuilder();
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;
@Override
protected void clear() {
this.complete = false;
}
@Override
public String getStartOfTag() {
return "<!"; //$NON-NLS-1$
}
@Override
protected String readTag() throws IOException {
int intChar;
char c;
StringBuilder node = new StringBuilder();
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;
@Override
protected void clear() {
this.complete = false;
}
@Override
public String getStartOfTag() {
return "<?"; //$NON-NLS-1$
}
@Override
protected String readTag() throws IOException {
int intChar;
char c;
StringBuilder node = new StringBuilder();
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 (TagReader tagReader : tagReaders) {
if (startOfTag.startsWith(tagReader.getStartOfTag())) {
tagReader.setReader(reader);
return tagReader;
}
}
// else
textNodeReader.setReader(reader);
return textNodeReader;
}
}
private static class TextReader extends TagReader {
private boolean complete;
private boolean isTextNode;
@Override
protected void clear() {
this.complete = false;
}
@Override
public String getStartOfTag() {
return IAntCoreConstants.EMPTY_STRING;
}
@Override
public boolean isTextNode() {
return this.isTextNode;
}
@Override
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' || whitespaceCharacter == '\r') {
node.append(whitespaceCharacter);
}
}
this.isTextNode = false;
} else {
this.isTextNode = true;
}
return node.toString();
}
@Override
public boolean requiresInitialIndent() {
return false;
}
@Override
public boolean startsOnNewline() {
return false;
}
}
private static class XmlElementReader extends TagReader {
private boolean complete = false;
@Override
protected void clear() {
this.complete = false;
}
@Override
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;
}
}
@Override
public int getPreTagDepthModifier() {
if (getTagText().startsWith("</")) { //$NON-NLS-1$
return -1;
}
return 0;
}
@Override
public String getStartOfTag() {
return "<"; //$NON-NLS-1$
}
@Override
protected String readTag() throws IOException {
StringBuilder node = new StringBuilder();
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;
private String fDefaultLineDelimiter;
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(fDefaultLineDelimiter);
}
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'}
*/
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';
}
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) {
StringBuilder indent = new StringBuilder(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;
}
public void setDefaultLineDelimiter(String defaultLineDelimiter) {
fDefaultLineDelimiter = defaultLineDelimiter;
}
}