/*****************************************************************************
* This file is part of Rinzo
*
* Author: Claudio Cancinos
* WWW: https://sourceforge.net/projects/editorxml
* Copyright (C): 2008, Claudio Cancinos
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; If not, see <http://www.gnu.org/licenses/>
****************************************************************************/
package ar.com.tadp.xml.rinzo.core.views;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import org.eclipse.jface.text.Assert;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
/**
*
* @author ccancinos
*/
public class XmlDocumentFormatter {
private static class CommentReader extends TagReader {
private boolean complete = false;
protected void clear() {
this.complete = false;
}
public String getStartOfTag() {
return "<!--";
}
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("-->")) {
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 "<?";
}
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];
}
}
textNodeReader.setReader(reader);
return textNodeReader;
}
}
private static class TextReader extends TagReader {
private boolean complete;
private boolean isTextNode;
protected void clear() {
this.complete = false;
}
public String getStartOfTag() {
return ""; //$NON-NLS-1$
}
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' || whitespaceCharacter == '\r') {
node.append(whitespaceCharacter);
}
}
this.isTextNode = false;
} else {
this.isTextNode = true;
}
return node.toString();
}
public boolean requiresInitialIndent() {
return false;
}
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("/ >")) {
return 0;
} else if (getTagText().startsWith("</")) {
return 0;
} else {
return +1;
}
}
public int getPreTagDepthModifier() {
if (getTagText().startsWith("</")) {
return -1;
}
return 0;
}
public String getStartOfTag() {
return "<";
}
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;
private String fDefaultLineDelimiter = "";
public XmlDocumentFormatter() {
super();
depth = -1;
}
private void copyNode(Reader reader, StringBuffer out) 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("\t"));
}
}
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';
}
public String format(String documentText) {
Assert.isNotNull(documentText);
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);
} else {
break;
}
}
reader.close();
} catch (IOException e) {
// XPathUtilLog.logError(XPathWorkbookActivator.getDefault(),
// e.toString());
}
return formattedXml.toString();
}
private boolean hasNewlineAlready(StringBuffer out) {
return out.lastIndexOf("\n") == formattedXml.length() - 1 || out.lastIndexOf("\r") == formattedXml.length() - 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) {
// XPathUtilLog.logError(XPathWorkbookActivator.getDefault(),
// e.toString());
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();
oneIndent.append('\t');
return oneIndent;
}
public void setDefaultLineDelimiter(String defaultLineDelimiter) {
fDefaultLineDelimiter = defaultLineDelimiter;
}
}