| /******************************************************************************* |
| * 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 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 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); |
| } |
| } |