/**
* This file Copyright (c) 2005-2008 Aptana, Inc. This program is
* dual-licensed under both the Aptana Public License and the GNU General
* Public license. You may elect to use one or the other of these licenses.
*
* This program is distributed in the hope that it will be useful, but
* AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or
* NONINFRINGEMENT. Redistribution, except as permitted by whichever of
* the GPL or APL you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or modify this
* program under the terms of the GNU General Public License,
* Version 3, as published by the Free Software Foundation. You should
* have received a copy of the GNU General Public License, Version 3 along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Aptana provides a special exception to allow redistribution of this file
* with certain Eclipse Public Licensed code and certain additional terms
* pursuant to Section 7 of the GPL. You may view the exception and these
* terms on the web at http://www.aptana.com/legal/gpl/.
*
* 2. For the Aptana Public License (APL), this program and the
* accompanying materials are made available under the terms of the APL
* v1.0 which accompanies this distribution, and is available at
* http://www.aptana.com/legal/apl/.
*
* You may view the GPL, Aptana's exception and additional terms, and the
* APL in the file titled license.html at the root of the corresponding
* plugin containing this source file.
*
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.ide.editor.xml.formatting;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Stack;
import org.eclipse.jface.preference.IPreferenceStore;
import com.aptana.ide.editor.html.preferences.IPreferenceConstants;
import com.aptana.ide.editor.xml.lexing.XMLTokenTypes;
import com.aptana.ide.editor.xml.parsing.XMLParseState;
import com.aptana.ide.editors.unified.ParentOffsetMapper;
import com.aptana.ide.lexer.Lexeme;
import com.aptana.ide.lexer.LexemeList;
/**
* XML formatting utils.
*
* @author Denis Denisenko
*/
public final class XMLUtils
{
/** is the tag "open" */
public static final int TAG_OPEN = 1;
/** is the tag "closed" */
public static final int TAG_CLOSED = 2;
/** is the tag "self-closed" */
public static final int TAG_SELF_CLOSED = 4;
/**
* Finds first lexeme in the list starting from the index specified. Found lexeme should be of the one of
* acceptedTypes type. Also the search breaks when meeting any lexeme the belongs to deniedTypes.
*
* @param lexemeList -
* lexemes.
* @param index -
* index to start from.
* @param acceptedTypes -
* accepted lexeme types.
* @param deniedTypes -
* types that the search breaks on.
* @return found lexeme, or null if not found.
*/
public static Lexeme getFirstLexemeBreaking(LexemeList lexemeList, int index, int[] acceptedTypes, int[] deniedTypes)
{
if (index >= lexemeList.size())
{
return null;
}
for (int i = index; i < lexemeList.size(); i++)
{
Lexeme currentLexeme = lexemeList.get(i);
// using row check is faster then creating hash sets here
// usually these type arrays are just in cpu cache
for (int j = 0; j < deniedTypes.length; j++)
{
if (currentLexeme.typeIndex == deniedTypes[j])
{
return null;
}
}
for (int j = 0; j < acceptedTypes.length; j++)
{
if (currentLexeme.typeIndex == acceptedTypes[j])
{
return currentLexeme;
}
}
}
return null;
}
/**
* Finds first lexeme in the list starting from the index specified. Found lexeme should be of the one of
* acceptedTypes type. Also the search breaks when meeting any lexeme that does not belong to typesToSkip.
*
* @param lexemeList -
* lexemes.
* @param index -
* index to start from.
* @param acceptedTypes -
* accepted lexeme types.
* @param typesToSkip -
* types that should be skipped.
* @return found lexeme, or null if not found.
*/
public static Lexeme getFirstLexemeSkipping(LexemeList lexemeList, int index, int[] acceptedTypes, int[] typesToSkip)
{
if (index >= lexemeList.size())
{
return null;
}
Main: for (int i = index; i < lexemeList.size(); i++)
{
Lexeme currentLexeme = lexemeList.get(i);
// using row check is faster then creating hash sets here
// usually these type arrays are just in cpu cache
for (int j = 0; j < acceptedTypes.length; j++)
{
if (currentLexeme.typeIndex == acceptedTypes[j])
{
return currentLexeme;
}
}
for (int j = 0; j < typesToSkip.length; j++)
{
if (currentLexeme.typeIndex == typesToSkip[j])
{
continue Main;
}
}
return null;
}
return null;
}
/**
* Returns true if we are somewhere inside the opening tag definition i.e. <tagname | >
*
* @param offset -
* offset to check.
* @param lexemeList -
* lexemes
* @return true if inside, false otherwise.
*/
public static boolean insideOpenTag(int offset, LexemeList lexemeList)
{
Lexeme startLexeme = getTagOpenLexeme(offset, lexemeList);
return (startLexeme != null);
}
/**
* Get tag open lexeme.
*
* @param offset -
* offset to check.
* @param lexemeList -
* lexemes.
* @return tag open lexeme, or null if not found.
*/
public static Lexeme getTagOpenLexeme(int offset, LexemeList lexemeList)
{
int index = ParentOffsetMapper.getLexemeIndexFromDocumentOffset(offset, lexemeList);
if (index > -1)
{
Lexeme l = lexemeList.get(index);
return getTagOpenLexeme(l, lexemeList);
}
else
{
return null;
}
}
/**
* Gets tag start lexeme.
*
* @param lexeme -
* lexeme to start search from.
* @param lexemeList -
* lexemes
* @return tag start lexeme, or null if not found.
*/
public static Lexeme getTagOpenLexeme(Lexeme lexeme, LexemeList lexemeList)
{
Lexeme startTag = null;
int position = lexemeList.getLexemeIndex(lexeme);
// backtrack over lexemes to find name - we are really just
// searching for the last OPEN_ELEMENT
while (position >= 0)
{
Lexeme curLexeme = lexemeList.get(position);
if (curLexeme.typeIndex == XMLTokenTypes.END_TAG)
{
break;
}
if (curLexeme.typeIndex == XMLTokenTypes.SLASH_GREATER_THAN)
{
break;
}
if (curLexeme.typeIndex == XMLTokenTypes.GREATER_THAN)
{
break;
}
if (curLexeme.typeIndex == XMLTokenTypes.START_TAG)
{
startTag = curLexeme;
break;
}
position--;
}
return startTag;
}
/**
* XMLUtils constructor.
*/
private XMLUtils()
{
}
/**
* getTagCloseLexeme
*
* @param offset
* @param lexemeList
* @return Lexeme
*/
public static Lexeme getTagCloseLexeme(int offset, LexemeList lexemeList)
{
Lexeme l = lexemeList.getCeilingLexeme(offset);
return getTagCloseLexeme(l, lexemeList);
}
/**
* getTagCloseLexeme
*
* @param startLexeme
* @param lexemeList
* @return Lexeme
*/
public static Lexeme getTagCloseLexeme(Lexeme startLexeme, LexemeList lexemeList)
{
return getNextLexemeOfType(startLexeme, new int[] { XMLTokenTypes.GREATER_THAN,
XMLTokenTypes.SLASH_GREATER_THAN }, lexemeList);
}
/**
* getNextLexemeOfType
*
* @param startLexeme
* @param lexemeTypes
* @param lexemeList
* @return Lexeme
*/
public static Lexeme getNextLexemeOfType(Lexeme startLexeme, int[] lexemeTypes, LexemeList lexemeList)
{
return getNextLexemeOfType(startLexeme, lexemeTypes, new int[0], lexemeList);
}
/**
* getNextLexemeOfType
*
* @param startLexeme
* @param lexemeTypes
* @param lexemeTypesToBail
* @param lexemeList
* @return Lexeme
*/
public static Lexeme getNextLexemeOfType(Lexeme startLexeme, int[] lexemeTypes, int[] lexemeTypesToBail,
LexemeList lexemeList)
{
Arrays.sort(lexemeTypes);
Arrays.sort(lexemeTypesToBail);
int index = lexemeList.getLexemeIndex(startLexeme);
for (int i = index; i < lexemeList.size(); i++)
{
Lexeme l = lexemeList.get(i);
if (Arrays.binarySearch(lexemeTypes, l.typeIndex) >= 0)
{
return l;
}
if (Arrays.binarySearch(lexemeTypesToBail, l.typeIndex) >= 0)
{
return null;
}
}
return null;
}
/**
* Is the current tag "closed", i.e. it has an greater than at the end of it?
*
* @param tag
* @param lexemeList
* @return isTagClosed
*/
public static int isTagClosed(Lexeme tag, LexemeList lexemeList)
{
// if(!isStartTag(tag) && isEndTag(tag))
// return false;
int position = lexemeList.getLexemeIndex(tag) + 1;
if (position < 0)
{
return TAG_OPEN;
}
// move forward over lexemes to find the closing tag element
while (position < lexemeList.size())
{
Lexeme curLexeme = lexemeList.get(position);
// if it's an end tag or a start tag, no
if (curLexeme.typeIndex == XMLTokenTypes.START_TAG || curLexeme.typeIndex == XMLTokenTypes.END_TAG)
{
return TAG_OPEN;
}
// if it's a slash greater than, we've self closed, so yes
if (curLexeme.typeIndex == XMLTokenTypes.SLASH_GREATER_THAN)
{
return TAG_SELF_CLOSED;
}
if (curLexeme.typeIndex == XMLTokenTypes.GREATER_THAN)
{
return TAG_CLOSED;
}
position++;
}
return TAG_OPEN;
}
/**
* isStartTag
*
* @param lexeme
* @return boolean
*/
public static boolean isStartTag(Lexeme lexeme)
{
return (lexeme.typeIndex == XMLTokenTypes.START_TAG || (lexeme.typeIndex == XMLTokenTypes.ERROR && lexeme
.getText().equals("<"))); //$NON-NLS-1$
}
/**
* isEndTag
*
* @param lexeme
* @return boolean
*/
public static boolean isEndTag(Lexeme lexeme)
{
return (lexeme.typeIndex == XMLTokenTypes.END_TAG || (lexeme.typeIndex == XMLTokenTypes.ERROR && lexeme
.getText().equals("</"))); //$NON-NLS-1$
}
/**
* Gets the open name of the tag in question, but trimmed such that we only create the name from the part _before_
* the offset (assuming the offset is inside the lexeme)
*
* @param tag
* The name of the tag
* @param offset
* @return A string with the tag name encased in "</>"
*/
public static String getOpenTagName(Lexeme tag, int offset)
{
String lexemeText = tag.getText();
if (tag.containsOffset(offset) && offset > tag.offset)
{
lexemeText = lexemeText.substring(0, offset - tag.offset);
}
String tempName = stripTagEndings(lexemeText);
return tempName;
}
/**
* Removes the "<" and "</" from the beginning and end of a tag
*
* @param tag
* The tag text to strip
* @return A string with the item removed
*/
public static String stripTagEndings(String tag)
{
String tempName = tag.replaceAll("</", ""); //$NON-NLS-1$ //$NON-NLS-2$
tempName = tempName.replaceAll(">", ""); //$NON-NLS-1$ //$NON-NLS-2$
return tempName.replaceAll("<", ""); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Creates a open tag for the tag name
*
* @param tagName
* The name of the tag
* @param close
* @return A string with the tag name encased in "<>"
*/
public static String createOpenTag(String tagName, boolean close)
{
String temp = "<" + stripTagEndings(tagName); //$NON-NLS-1$
if (close)
{
return temp + ">"; //$NON-NLS-1$
}
else
{
return temp;
}
}
/**
* Creates a open tag for the tag name taht self-closes
*
* @param tagName
* The name of the tag
* @return A string with the tag name encased in "< />"
*/
public static String createSelfClosedTag(String tagName)
{
String temp = "<" + stripTagEndings(tagName); //$NON-NLS-1$
return temp + " />"; //$NON-NLS-1$
}
/**
* Creates a close tag for the tag name
*
* @param tag
* The opening lexeme
* @param close
* @return A string with the tag name encased in "</>"
*/
public static String createCloseTag(Lexeme tag, boolean close)
{
if (tag.typeIndex == XMLTokenTypes.START_TAG)
{
return createCloseTag(tag.getText().substring(1), close);
}
else
{
return null;
}
}
/**
* Creates a close tag for the tag name
*
* @param tagName
* The name of the tag
* @param close
* @return A string with the tag name encased in "</>"
*/
public static String createCloseTag(String tagName, boolean close)
{
String temp = "</" + stripTagEndings(tagName); //$NON-NLS-1$
if (close)
{
return temp + ">"; //$NON-NLS-1$
}
else
{
return temp;
}
}
/**
* Creates a close tag for the tag name, but trimmed such that we only create the close tag from the part _before_
* the offset (assuming the offset is inside the lexeme)
*
* @param tag
* The name of the tag
* @param offset
* @param close
* @return A string with the tag name encased in "</>"
*/
public static String createCloseTag(Lexeme tag, int offset, boolean close)
{
String tempName = getOpenTagName(tag, offset);
return createCloseTag(tempName, close);
}
/**
* Surrounds the item with quotes according to the current preferences
*
* @param store
* @param value
* @return String
*/
public static String quoteAttributeValue(IPreferenceStore store, String value)
{
String quoteChar = ""; //$NON-NLS-1$
return quoteChar + value + quoteChar;
}
/**
* Do we insert an equals sign?
*
* @param store
* @return String
*/
public static boolean insertEquals(IPreferenceStore store)
{
if (store != null)
{
return store.getBoolean(IPreferenceConstants.HTMLEDITOR_INSERT_EQUALS);
}
else
{
return false;
}
}
/**
* Given an opening and a closing lexeme of an HTML open tag declaration it will grab all of the attributes inside
* as a hashtable
*
* @param openTagLexeme
* @param closeTagLexeme
* @param lexemeList
* @return Hashtable
*/
public static Hashtable gatherAttributes(Lexeme openTagLexeme, Lexeme closeTagLexeme, LexemeList lexemeList)
{
Hashtable<String, String> h = new Hashtable<String, String>();
if (openTagLexeme == null || closeTagLexeme == null)
{
return h;
}
int startIndex = lexemeList.getLexemeIndex(openTagLexeme);
int endIndex = lexemeList.size() - 1;
if (closeTagLexeme != null)
{
endIndex = lexemeList.getLexemeIndex(closeTagLexeme);
}
if (startIndex > endIndex)
{
throw new IndexOutOfBoundsException(""); //$NON-NLS-1$
}
String currentName = null;
boolean foundEquals = false;
boolean foundQuote = false;
while (startIndex < endIndex)
{
startIndex++;
Lexeme l = lexemeList.get(startIndex);
if (l.typeIndex == XMLTokenTypes.NAME && !foundEquals)
{
currentName = l.getText();
continue;
}
if (l.typeIndex == XMLTokenTypes.EQUAL)
{
foundEquals = true;
continue;
}
else if (l.typeIndex == XMLTokenTypes.STRING || (foundEquals && l.typeIndex == XMLTokenTypes.NAME))
{
foundEquals = false;
if (currentName != null && !h.containsKey(currentName))
{
h.put(currentName, l.getText());
}
}
else if (l.typeIndex == XMLTokenTypes.STRING)
{
if (foundQuote == true)
{
foundQuote = false;
foundEquals = false;
if (currentName != null && !h.containsKey(currentName))
{
h.put(currentName, ""); //$NON-NLS-1$
}
}
else
{
foundQuote = true;
}
}
}
return h;
}
/**
* Are we inside a lexeme where it is "quoted"
*
* @param lexeme
* The string text
* @param offset
* The current cursor offset
* @return Yes if true, false if not
*/
public static boolean insideQuotedString(Lexeme lexeme, int offset)
{
String text = lexeme.getText();
if ((text.startsWith("\"") || text.startsWith("'")) && lexeme.containsOffset(offset)) //$NON-NLS-1$ //$NON-NLS-2$
{
return true;
}
return false;
}
/**
* getPreviousUnclosedTag
*
* @param lexeme
* @param lexemeList
* @param parseState
* @return Lexeme
*/
public static Lexeme getPreviousUnclosedTag(Lexeme lexeme, LexemeList lexemeList, XMLParseState parseState)
{
if (lexeme == null)
{
return null;
}
int position = lexemeList.getLexemeIndex(lexeme) - 1;
Lexeme startTag = null;
boolean selfClosed = false;
Stack<Lexeme> tags = new Stack<Lexeme>();
// backtrack over lexemes to find name - we are really just
// searching for the last OPEN_ELEMENT
while (position >= 0)
{
Lexeme curLexeme = lexemeList.get(position);
if (curLexeme.typeIndex == XMLTokenTypes.END_TAG)
{
tags.push(curLexeme);
}
if (curLexeme.typeIndex == XMLTokenTypes.SLASH_GREATER_THAN)
{
selfClosed = true;
}
if (curLexeme.typeIndex == XMLTokenTypes.START_TAG)
{
// if the item was self-closed, just continue on
if (selfClosed)
{
selfClosed = false;
position--;
continue;
}
if (tags.size() == 0)
{
return curLexeme;
}
Lexeme l = tags.pop();
if (l.typeIndex == XMLTokenTypes.SLASH_GREATER_THAN)
{
position--;
continue;
}
position--;
continue;
}
position--;
}
return startTag;
}
/**
* Is the current start tag "balanced" by a later closing tag?
*
* @param tag -
* lexeme of the start tag to check
* @param lexemeList -
* list of lexemes
* @param parseState -
* parse state
* @return true if tag is balanced
*/
public static boolean isStartTagBalanced(Lexeme tag, LexemeList lexemeList, XMLParseState parseState)
{
// If we are the last lexeme in the list, there is no way we can be balanced.
int index = lexemeList.getLexemeIndex(tag);
if (index == lexemeList.size() - 1)
{
return false;
}
if (tag == null || lexemeList == null || parseState == null)
{
throw new IllegalArgumentException("null arguments are not accepted"); //$NON-NLS-1$
}
if (!isStartTag(tag))
{
// maybe IllegalArgumentException would be better @Denis
return false;
}
// treating self-closed tags as always balanced
if (isTagSelfClosed(tag, lexemeList))
{
return true;
}
String originalTagName = stripTagEndings(tag.getText());
// tags that are able closing themselves are always balanced
int balance = 0;
// current state: whether we're tracking tag close or not
boolean trackingClose = false;
for (int i = 0; i < lexemeList.size(); i++)
{
Lexeme currentLexeme = lexemeList.get(i);
if (isStartTag(currentLexeme))
{
// checking tag name
String currenTagName = stripTagEndings(currentLexeme.getText());
if (originalTagName.equals(currenTagName))
{
// entering trackingClose state
trackingClose = true;
}
}
else if (isEndTag(currentLexeme))
{
// if we met the end tag, we should always escape "trackingClose" state
if (trackingClose)
{
balance++;
trackingClose = false;
}
// checking tag name
String currenTagName = stripTagEndings(currentLexeme.getText());
if (originalTagName.equals(currenTagName))
{
balance--;
}
}
// if we met some other lexeme and should track the close of the opened tag...
else if (trackingClose)
{
// if it's an end tag or a start tag, no
if (currentLexeme.typeIndex == XMLTokenTypes.START_TAG
|| currentLexeme.typeIndex == XMLTokenTypes.END_TAG)
{
balance++;
trackingClose = false;
}
// if it's a slash greater than, we've self closed, so yes
else if (currentLexeme.typeIndex == XMLTokenTypes.SLASH_GREATER_THAN)
{
trackingClose = false;
}
else if (currentLexeme.typeIndex == XMLTokenTypes.GREATER_THAN)
{
balance++;
trackingClose = false;
}
}
// if (isStartTag(currentLexeme) && isTagClosed(currentLexeme, lexemeList) != TAG_SELF_CLOSED) {
// String currenTagName = stripTagEndings(currentLexeme.getText());
// if (originalTagName.equals(currenTagName))
// {
// balance++;
// }
// }
// else if (isEndTag(currentLexeme))
// {
// String currenTagName = stripTagEndings(currentLexeme.getText());
// if (originalTagName.equals(currenTagName))
// {
// balance--;
// }
// }
}
return balance <= 0;
}
/**
* Checks whether tag is self-closed
*
* @param tag -
* tag to check
* @param lexemeList -
* list of lexemes
* @return whether tag is self-closed
*/
public static boolean isTagSelfClosed(Lexeme tag, LexemeList lexemeList)
{
if (!isStartTag(tag))
{
return false;
}
int position = lexemeList.getLexemeIndex(tag) + 1;
if (position < 0)
{
return false;
}
// move forward over lexemes to find name - we are really just
// searching for the next END_TAG
while (position < lexemeList.size())
{
Lexeme curLexeme = lexemeList.get(position);
// if it's a slash greater than, we've self closed
if (curLexeme.typeIndex == XMLTokenTypes.SLASH_GREATER_THAN)
{
return true;
}
// If it's an end tag, we haven't
if (curLexeme.typeIndex == XMLTokenTypes.END_TAG)
{
return false;
}
// Tag close sign found, we haven't
if (curLexeme.typeIndex == XMLTokenTypes.GREATER_THAN)
{
return false;
}
// if it's a start tag, we haven't
if (curLexeme.typeIndex == XMLTokenTypes.START_TAG)
{
return false;
}
position++;
}
return false;
}
}