blob: 4ef4a695a2bbd44fe1ff75dcc9c2f1265b9cd9a8 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2005 John-Mason P. Shackelford and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* John-Mason P. Shackelford - initial API and implementation
* IBM Corporation - Bug 73411, 84342
*******************************************************************************/
package org.eclipse.ant.internal.ui.editor.formatter;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class XmlTagFormatter {
protected static class AttributePair {
private String fAttribute;
private String fValue;
private char fQuote;
public AttributePair(String attribute, String value, char attributeQuote) {
fAttribute = attribute;
fValue = value;
fQuote= attributeQuote;
}
public String getAttribute() {
return fAttribute;
}
public String getValue() {
return fValue;
}
public char getQuote() {
return fQuote;
}
}
protected static class ParseException extends Exception {
private static final long serialVersionUID = 1L;
public ParseException(String message) {
super(message);
}
}
protected static class Tag {
private List fAttributes = new ArrayList();
private boolean fClosed;
private String fElementName;
public void addAttribute(String attribute, String value, char quote) {
fAttributes.add(new AttributePair(attribute, value, quote));
}
public int attributeCount() {
return fAttributes.size();
}
public AttributePair getAttributePair(int i) {
return (AttributePair) fAttributes.get(i);
}
public String getElementName() {
return this.fElementName;
}
public boolean isClosed() {
return fClosed;
}
public int minimumLength() {
int length = 2; // for the < >
if (this.isClosed()) length++; // if we need to add an />
length += this.getElementName().length();
if (this.attributeCount() > 0 || this.isClosed()) length++;
for (int i = 0; i < this.attributeCount(); i++) {
AttributePair attributePair = this.getAttributePair(i);
length += attributePair.getAttribute().length();
length += attributePair.getValue().length();
length += 4; // equals sign, quote characters & trailing space
}
if (this.attributeCount() > 0 && !this.isClosed()) length--;
return length;
}
public void setAttributes(List attributePair) {
fAttributes.clear();
fAttributes.addAll(attributePair);
}
public void setClosed(boolean closed) {
fClosed = closed;
}
public void setElementName(String elementName) {
fElementName = elementName;
}
public String toString() {
StringBuffer sb = new StringBuffer(500);
sb.append('<');
sb.append(this.getElementName());
if (this.attributeCount() > 0 || this.isClosed()) sb.append(' ');
for (int i = 0; i < this.attributeCount(); i++) {
AttributePair attributePair = this.getAttributePair(i);
sb.append(attributePair.getAttribute());
sb.append('=');
sb.append(attributePair.getQuote());
sb.append(attributePair.getValue());
sb.append(attributePair.getQuote());
if (this.isClosed() || i != this.attributeCount() - 1)
sb.append(' ');
}
if (this.isClosed()) sb.append('/');
sb.append('>');
return sb.toString();
}
}
protected static class TagFormatter {
private int countChar(char searchChar, String inTargetString) {
StringCharacterIterator iter = new StringCharacterIterator(
inTargetString);
int i = 0;
if (iter.first() == searchChar) i++;
while (iter.getIndex() < iter.getEndIndex()) {
if (iter.next() == searchChar) {
i++;
}
}
return i;
}
public String format(Tag tag, FormattingPreferences prefs, String indent, String lineDelimiter) {
if (prefs.wrapLongTags()
&& lineRequiresWrap(indent + tag.toString(), prefs
.getMaximumLineWidth(), prefs.getTabWidth())) {
return wrapTag(tag, prefs, indent, lineDelimiter);
}
return tag.toString();
}
protected boolean lineRequiresWrap(String line, int lineWidth, int tabWidth) {
return tabExpandedLineWidth(line, tabWidth) > lineWidth;
}
/**
* @param line
* the line in which spaces are to be expanded
* @param tabWidth
* number of spaces to substitute for a tab
* @return length of the line with tabs expanded to spaces
*/
protected int tabExpandedLineWidth(String line, int tabWidth) {
int tabCount = countChar('\t', line);
return (line.length() - tabCount) + (tabCount * tabWidth);
}
protected String wrapTag(Tag tag, FormattingPreferences prefs, String indent, String lineDelimiter) {
StringBuffer sb = new StringBuffer(1024);
sb.append('<');
sb.append(tag.getElementName());
sb.append(' ');
if (tag.attributeCount() > 0) {
AttributePair pair= tag.getAttributePair(0);
sb.append(pair.getAttribute());
sb.append('=');
sb.append(pair.getQuote());
sb.append(tag.getAttributePair(0).getValue());
sb.append(pair.getQuote());
}
if (tag.attributeCount() > 1) {
char[] extraIndent = new char[tag.getElementName().length() + 2];
Arrays.fill(extraIndent, ' ');
for (int i = 1; i < tag.attributeCount(); i++) {
AttributePair pair= tag.getAttributePair(i);
sb.append(lineDelimiter);
sb.append(indent);
sb.append(extraIndent);
sb.append(pair.getAttribute());
sb.append('=');
sb.append(pair.getQuote());
sb.append(pair.getValue());
sb.append(pair.getQuote());
}
}
if (prefs.alignElementCloseChar()) {
sb.append(lineDelimiter);
sb.append(indent);
} else if (tag.isClosed()) {
sb.append(' ');
}
if (tag.isClosed()) sb.append('/');
sb.append('>');
return sb.toString();
}
}
// if object creation is an issue, use static methods or a flyweight
// pattern
protected static class TagParser {
private String fElementName;
private String fParseText;
protected List getAttibutes(String elementText)
throws ParseException {
class Mode {
private int mode;
public void setAttributeNameSearching() {mode = 0;}
public void setAttributeNameFound() {mode = 1;}
public void setAttributeValueSearching() {mode = 2;}
public void setAttributeValueFound() {mode = 3;}
public void setFinished() {mode = 4;}
public boolean isAttributeNameSearching() {return mode == 0;}
public boolean isAttributeNameFound() {return mode == 1;}
public boolean isAttributeValueSearching() {return mode == 2;}
public boolean isAttributeValueFound() {return mode == 3;}
public boolean isFinished() {return mode == 4;}
}
List attributePairs = new ArrayList();
CharacterIterator iter = new StringCharacterIterator(elementText
.substring(getElementName(elementText).length() + 2));
// state for finding attributes
Mode mode = new Mode();
mode.setAttributeNameSearching();
char attributeQuote = '"';
StringBuffer currentAttributeName = null;
StringBuffer currentAttributeValue = null;
char c = iter.first();
while (iter.getIndex() < iter.getEndIndex()) {
switch (c) {
case '"':
case '\'':
if (mode.isAttributeValueSearching()) {
// start of an attribute value
attributeQuote = c;
mode.setAttributeValueFound();
currentAttributeValue = new StringBuffer(1024);
} else if (mode.isAttributeValueFound()
&& attributeQuote == c) {
// we've completed a pair!
AttributePair pair = new AttributePair(
currentAttributeName.toString(),
currentAttributeValue.toString(), attributeQuote);
attributePairs.add(pair);
// start looking for another attribute
mode.setAttributeNameSearching();
} else if (mode.isAttributeValueFound()
&& attributeQuote != c) {
// this quote character is part of the attribute value
currentAttributeValue.append(c);
} else {
// this is no place for a quote!
throw new ParseException("Unexpected '" + c //$NON-NLS-1$
+ "' when parsing:\n\t" + elementText); //$NON-NLS-1$
}
break;
case '=':
if (mode.isAttributeValueFound()) {
// this character is part of the attribute value
currentAttributeValue.append(c);
} else if (mode.isAttributeNameFound()) {
// end of the name, now start looking for the value
mode.setAttributeValueSearching();
} else {
// this is no place for an equals sign!
throw new ParseException("Unexpected '" + c //$NON-NLS-1$
+ "' when parsing:\n\t" + elementText); //$NON-NLS-1$
}
break;
case '/':
case '>':
if (mode.isAttributeValueFound()) {
// attribute values are CDATA, add it all
currentAttributeValue.append(c);
} else if (mode.isAttributeNameSearching()) {
mode.setFinished();
} else if (mode.isFinished()){
// consume the remaining characters
} else {
// we aren't ready to be done!
throw new ParseException("Unexpected '" + c //$NON-NLS-1$
+ "' when parsing:\n\t" + elementText); //$NON-NLS-1$
}
break;
default:
if (mode.isAttributeValueFound()) {
// attribute values are CDATA, add it all
currentAttributeValue.append(c);
} else if (mode.isFinished()) {
if (!Character.isWhitespace(c)) {
throw new ParseException("Unexpected '" + c //$NON-NLS-1$
+ "' when parsing:\n\t" + elementText); //$NON-NLS-1$
}
} else {
if (!Character.isWhitespace(c)) {
if (mode.isAttributeNameSearching()) {
// we found the start of an attribute name
mode.setAttributeNameFound();
currentAttributeName = new StringBuffer(255);
currentAttributeName.append(c);
} else if (mode.isAttributeNameFound()) {
currentAttributeName.append(c);
}
}
}
break;
}
c = iter.next();
}
if (!mode.isFinished()) {
throw new ParseException("Element did not complete normally."); //$NON-NLS-1$
}
return attributePairs;
}
/**
* @param tagText
* text of an XML tag
* @return extracted XML element name
*/
protected String getElementName(String tagText) throws ParseException {
if (!tagText.equals(this.fParseText) || this.fElementName == null) {
int endOfTag = tagEnd(tagText);
if ((tagText.length() > 2) && (endOfTag > 1)) {
this.fParseText = tagText;
this.fElementName = tagText.substring(1, endOfTag);
} else {
throw new ParseException("No element name for the tag:\n\t" //$NON-NLS-1$
+ tagText);
}
}
return fElementName;
}
protected boolean isClosed(String tagText) {
return tagText.charAt(tagText.lastIndexOf('>') - 1) == '/';
}
/**
* @param tagText
* @return a fully populated tag
*/
public Tag parse(String tagText) throws ParseException {
Tag tag = new Tag();
tag.setElementName(getElementName(tagText));
tag.setAttributes(getAttibutes(tagText));
tag.setClosed(isClosed(tagText));
return tag;
}
private int tagEnd(String text) {
// This is admittedly a little loose, but we don't want the
// formatter to be too strict...
// http://www.w3.org/TR/2000/REC-xml-20001006#NT-Name
for (int i = 1; i < text.length(); i++) {
char c = text.charAt(i);
if (!Character.isLetterOrDigit(c) && c != ':' && c != '.'
&& c != '-' && c != '_') { return i; }
}
return -1;
}
}
public static String format(String tagText, FormattingPreferences prefs, String indent, String lineDelimiter) {
Tag tag;
if (tagText.startsWith("</") || tagText.startsWith("<%") //$NON-NLS-1$ //$NON-NLS-2$
|| tagText.startsWith("<?") || tagText.startsWith("<[")) { //$NON-NLS-1$ //$NON-NLS-2$
return tagText;
}
try {
tag = new TagParser().parse(tagText);
} catch (ParseException e) {
// if we can't parse the tag, give up and leave the text as is.
return tagText;
}
return new TagFormatter().format(tag, prefs, indent, lineDelimiter);
}
}