content: // Filename: SyntaxDocument.java
  // http://forums.sun.com/thread.jspa?threadID=743407&messageID=9497681
  // http://www.dound.com/src/MultiSyntaxDocument.java
  
  /**
   * (C) camickr (primary author; java sun forums user)
   * (C) David Underhill
   * (C) 2009 University of Waikato, Hamilton, New Zealand
   */
  
  package weka.gui.scripting;
  
  import weka.gui.visualize.VisualizeUtils;
  
  import java.awt.Color;
  import java.awt.Font;
  import java.awt.FontMetrics;
  import java.awt.Toolkit;
  import java.util.HashMap;
  import java.util.Properties;
  
  import javax.swing.text.AttributeSet;
  import javax.swing.text.BadLocationException;
  import javax.swing.text.DefaultEditorKit;
  import javax.swing.text.DefaultStyledDocument;
  import javax.swing.text.Element;
  import javax.swing.text.MutableAttributeSet;
  import javax.swing.text.SimpleAttributeSet;
  import javax.swing.text.StyleConstants;
  import javax.swing.text.TabSet;
  import javax.swing.text.TabStop;
  
  /**
   * Highlights syntax in a DefaultStyledDocument. Allows any number of keywords
   * to be formatted in any number of user-defined styles.
   * 
   * @author camickr (primary author; java sun forums user)
   * @author David Underhill
   * @author FracPete (fracpete at waikato dot ac dot nz) - use of a properties file to setup syntax highlighting instead of hard-coded
   */
  public class SyntaxDocument
    extends DefaultStyledDocument {
  
    /** for serialization. */
    protected static final long serialVersionUID = -3642426465631271381L;
  
    /** the maximum number of tabs. */
    public static final int MAX_TABS = 35;
    
    /** the font family. */
    public static final String DEFAULT_FONT_FAMILY = "monospaced";
  
    /** the font size. */
    public static final int DEFAULT_FONT_SIZE = 12;
  
    /** the attribute set for normal code. */
    public static final SimpleAttributeSet DEFAULT_NORMAL;
  
    /** the attribute set for comments. */
    public static final SimpleAttributeSet DEFAULT_COMMENT;
  
    /** the attribute set for strings. */
    public static final SimpleAttributeSet DEFAULT_STRING;
  
    /** the attribute set for keywords. */
    public static final SimpleAttributeSet DEFAULT_KEYWORD;
  
    static {
      DEFAULT_NORMAL = new SimpleAttributeSet();
      StyleConstants.setForeground(DEFAULT_NORMAL, Color.BLACK);
      StyleConstants.setFontFamily(DEFAULT_NORMAL, DEFAULT_FONT_FAMILY);
      StyleConstants.setFontSize(DEFAULT_NORMAL, DEFAULT_FONT_SIZE);
  
      DEFAULT_COMMENT = new SimpleAttributeSet();
      StyleConstants.setForeground(DEFAULT_COMMENT, Color.GRAY);
      StyleConstants.setFontFamily(DEFAULT_COMMENT, DEFAULT_FONT_FAMILY);
      StyleConstants.setFontSize(DEFAULT_COMMENT, DEFAULT_FONT_SIZE);
  
      DEFAULT_STRING = new SimpleAttributeSet();
      StyleConstants.setForeground(DEFAULT_STRING, Color.RED);
      StyleConstants.setFontFamily(DEFAULT_STRING, DEFAULT_FONT_FAMILY);
      StyleConstants.setFontSize(DEFAULT_STRING, DEFAULT_FONT_SIZE);
  
      // default style for new keyword types
      DEFAULT_KEYWORD = new SimpleAttributeSet();
      StyleConstants.setForeground(DEFAULT_KEYWORD, Color.BLUE);
      StyleConstants.setBold(DEFAULT_KEYWORD, false);
      StyleConstants.setFontFamily(DEFAULT_KEYWORD, DEFAULT_FONT_FAMILY);
      StyleConstants.setFontSize(DEFAULT_KEYWORD, DEFAULT_FONT_SIZE);
    }
  
    /**
     * The attribute type.
     */
    public enum ATTR_TYPE {
      /** normal string. */
      Normal,
      /** a comment. */
      Comment,
      /** a quoted string. */
      Quote;
    }
  
    /** the document. */
    protected DefaultStyledDocument m_Self;
  
    /** the root element. */
    protected Element m_RootElement;
  
    /** whether we're currently in a multi-line comment. */
    protected boolean m_InsideMultiLineComment;
  
    /** the keywords. */
    protected HashMap<String, MutableAttributeSet> m_Keywords;
    
    /** the delimiters. */
    protected String m_Delimiters;
    
    /** the quote delimiter. */
    protected String m_QuoteDelimiters;
    
    /** the quote escape. */
    protected String m_QuoteEscape;
    
    /** the multi-line comment start. */
    protected String m_MultiLineCommentStart;
    
    /** the multi-line comment end. */
    protected String m_MultiLineCommentEnd;
    
    /** the single-line comment start. */
    protected String m_SingleLineCommentStart;
  
    /** the start of a block. */
    protected String m_BlockStart;
  
    /** the end of a block. */
    protected String m_BlockEnd;
    
    /** the font size. */
    protected int m_FontSize;
  
    /** the font name. */
    protected String m_FontName;
    
    /** the background color. */
    protected Color m_BackgroundColor;
    
    /** the number of spaces used for indentation. */
    protected String m_Indentation;
  
    /** whether to add matching brackets. */
    protected boolean m_AddMatchingEndBlocks;
    
    /** whether to use blanks instead of tabs. */
    protected boolean m_UseBlanks;
    
    /** whether multi-line comments are enabled. */
    protected boolean m_MultiLineComment;
    
    /** whether keywords are case-sensitive. */
    protected boolean m_CaseSensitive;
    
    /**
     * Initializes the document.
     * 
     * @param props	the properties to obtain the setup from
     */
    public SyntaxDocument(Properties props) {
      m_Self        = this;
      m_RootElement = m_Self.getDefaultRootElement();
      m_Keywords    = new HashMap<String, MutableAttributeSet>();
      m_FontSize    = DEFAULT_FONT_SIZE;
      m_FontName    = DEFAULT_FONT_FAMILY;
      putProperty(DefaultEditorKit.EndOfLineStringProperty, "\n");
      
      setup(props);
    }
    
    /**
     * Sets up the document according to the properties.
     * 
     * @param props	the properties to use
     */
    protected void setup(Properties props) {
      setDelimiters(props.getProperty("Delimiters", ";:{}()[]+-/%<=>!&|^~*"));
      setQuoteDelimiters(props.getProperty("QuoteDelimiters", "\"\'"));
      setQuoteEscape(props.getProperty("QuoteEscape", "\\"));
      setSingleLineCommentStart(props.getProperty("SingleLineCommentStart", "//"));
      setMultiLineComment(props.getProperty("MultiLineComment", "false").equals("true"));
      setMultiLineCommentStart(props.getProperty("MultiLineCommentStart", "/*"));
      setMultiLineCommentEnd(props.getProperty("MultiLineCommentEnd", "*/"));
      setBlockStart(props.getProperty("BlockStart", "{"));
      setBlockEnd(props.getProperty("BlockEnd", "}"));
      setAddMatchingEndBlocks(props.getProperty("AddMatchingBlockEnd", "false").equals("true"));
      setUseBlanks(props.getProperty("UseBlanks", "false").equals("true"));
      setCaseSensitive(props.getProperty("CaseSensitive", "true").equals("true"));
      addKeywords(props.getProperty("Keywords", "").trim().replaceAll(" ", "").split(","), DEFAULT_KEYWORD);
      setTabs(Integer.parseInt(props.getProperty("Tabs", "2")));
      setAttributeColor(DEFAULT_NORMAL, VisualizeUtils.processColour(props.getProperty("ForegroundColor", "black"), Color.BLACK));
      setAttributeColor(DEFAULT_COMMENT, VisualizeUtils.processColour(props.getProperty("CommentColor", "gray"), Color.GRAY));
      setAttributeColor(DEFAULT_STRING, VisualizeUtils.processColour(props.getProperty("StringColor", "red"), Color.RED));
      setAttributeColor(DEFAULT_KEYWORD, VisualizeUtils.processColour(props.getProperty("KeywordColor", "blue"), Color.BLUE));
      setBackgroundColor(VisualizeUtils.processColour(props.getProperty("BackgroundColor", "white"), Color.WHITE));
      setFontName(props.getProperty("FontName", "monospaced"));
      setFontSize(Integer.parseInt(props.getProperty("FontSize", "12")));
      setIndentationSize(Integer.parseInt(props.getProperty("Indentation", "2")));
    }
  
    /**
     * Sets the font of the specified attribute.
     * 
     * @param attr
     *          the attribute to apply this font to (normal, comment, string)
     * @param style
     *          font style (Font.BOLD, Font.ITALIC, Font.PLAIN)
     */
    public void setAttributeFont(ATTR_TYPE attr, int style) {
      Font f = new Font(m_FontName, style, m_FontSize);
  
      if (attr == ATTR_TYPE.Comment)
        setAttributeFont(DEFAULT_COMMENT, f);
      else if (attr == ATTR_TYPE.Quote)
        setAttributeFont(DEFAULT_STRING, f);
      else
        setAttributeFont(DEFAULT_NORMAL, f);
    }
  
    /**
     * Sets the font of the specified attribute.
     * 
     * @param attr
     *          attribute to apply this font to
     * @param f
     *          the font to use
     */
    public static void setAttributeFont(MutableAttributeSet attr, Font f) {
      StyleConstants.setBold(attr, f.isBold());
      StyleConstants.setItalic(attr, f.isItalic());
      StyleConstants.setFontFamily(attr, f.getFamily());
      StyleConstants.setFontSize(attr, f.getSize());
    }
  
    /**
     * Sets the foreground (font) color of the specified attribute.
     * 
     * @param attr
     *          the attribute to apply this font to (normal, comment, string)
     * @param c
     *          the color to use
     */
    public void setAttributeColor(ATTR_TYPE attr, Color c) {
      if (attr == ATTR_TYPE.Comment)
        setAttributeColor(DEFAULT_COMMENT, c);
      else if (attr == ATTR_TYPE.Quote)
        setAttributeColor(DEFAULT_STRING, c);
      else
        setAttributeColor(DEFAULT_NORMAL, c);
    }
  
    /**
     * Sets the foreground (font) color of the specified attribute.
     * 
     * @param attr
     *          attribute to apply this color to
     * @param c
     *          the color to use
     */
    public static void setAttributeColor(MutableAttributeSet attr, Color c) {
      StyleConstants.setForeground(attr, c);
    }
  
    /**
     * Associates the keywords with a particular formatting style.
     * 
     * @param keywords
     *          the tokens or words to format
     * @param attr
     *          how to format the keywords
     */
    public void addKeywords(String[] keywords, MutableAttributeSet attr) {
      int		i;
      
      for (i = 0; i < keywords.length; i++)
        addKeyword(keywords[i], attr);
    }
    
    /**
     * Associates a keyword with a particular formatting style.
     * 
     * @param keyword
     *          the token or word to format
     * @param attr
     *          how to format keyword
     */
    public void addKeyword(String keyword, MutableAttributeSet attr) {
      if (m_CaseSensitive)
        m_Keywords.put(keyword, attr);
      else
        m_Keywords.put(keyword.toLowerCase(), attr);
    }
  
    /**
     * Gets the formatting for a keyword.
     * 
     * @param keyword
     *          the token or word to stop formatting
     * 
     * @return how keyword is formatted, or null if no formatting is applied to it
     */
    public MutableAttributeSet getKeywordFormatting(String keyword) {
      if (m_CaseSensitive)
        return m_Keywords.get(keyword);
      else
        return m_Keywords.get(keyword.toLowerCase());
    }
  
    /**
     * Removes an association between a keyword with a particular formatting style.
     * 
     * @param keyword
     *          the token or word to stop formatting
     */
    public void removeKeyword(String keyword) {
      if (m_CaseSensitive)
        m_Keywords.remove(keyword);
      else
        m_Keywords.remove(keyword.toLowerCase());
    }
  
    /** 
     * sets the number of characters per tab.
     * 
     * @param charactersPerTab
     * 		the characters per tab
     */
    public void setTabs(int charactersPerTab) {
      Font f = new Font(m_FontName, Font.PLAIN, m_FontSize);
  
      FontMetrics fm = Toolkit.getDefaultToolkit().getFontMetrics(f);
      int charWidth = fm.charWidth('w');
      int tabWidth = charWidth * charactersPerTab;
  
      TabStop[] tabs = new TabStop[MAX_TABS];
  
      for (int j = 0; j < tabs.length; j++)
        tabs[j] = new TabStop((j+1) * tabWidth);
  
      TabSet tabSet = new TabSet(tabs);
      SimpleAttributeSet attributes = new SimpleAttributeSet();
      StyleConstants.setTabSet(attributes, tabSet);
      int length = getLength();
      setParagraphAttributes(0, length, attributes, false);
    }
  
    /**
     * Override to apply syntax highlighting after the document has been updated.
     * 
     * @param offset
     * 		the offset
     * @param str
     * 		the string to insert
     * @param a
     * 		the attribute set, can be null
     * @throws BadLocationException
     * 		if offset is invalid
     */
    public void insertString(int offset, String str, AttributeSet a)
        throws BadLocationException {
      if (m_AddMatchingEndBlocks && (m_BlockStart.length() > 0) && str.equals(m_BlockStart))
        str = addMatchingBlockEnd(offset);
      else if (m_UseBlanks && str.equals("\t"))
        str = m_Indentation;
      
      super.insertString(offset, str, a);
      processChangedLines(offset, str.length());
    }
  
    /**
     * Applies syntax highlighting after the document has been updated.
     * 
     * @param offset
     * 		the offset of the deletion
     * @param length
     * 		the length of the deletion
     * @throws BadLocationException
     * 		if offsets are invalid
     */
    public void remove(int offset, int length) throws BadLocationException {
      super.remove(offset, length);
      processChangedLines(offset, 0);
    }
  
    /**
     * Determine how many lines have been changed, then apply highlighting to each
     * line.
     * 
     * @param offset
     * 		the offset of the changed lines
     * @param length
     * 		the length of the change
     * @throws BadLocationException
     * 		if offset is invalid
     */
    public void processChangedLines(int offset, int length)
        throws BadLocationException {
      String content = m_Self.getText(0, m_Self.getLength());
  
      // The lines affected by the latest document update
  
      int startLine = m_RootElement.getElementIndex(offset);
      int endLine = m_RootElement.getElementIndex(offset + length);
  
      // Make sure all comment lines prior to the start line are commented
      // and determine if the start line is still in a multi line comment
  
      if (getMultiLineComment())
        setInsideMultiLineComment(commentLinesBefore(content, startLine));
  
      // Do the actual highlighting
  
      for (int i = startLine; i <= endLine; i++) {
        applyHighlighting(content, i);
      }
  
      // Resolve highlighting to the next end multi line delimiter
  
      if (isMultiLineComment())
        commentLinesAfter(content, endLine);
      else
        highlightLinesAfter(content, endLine);
    }
  
    /**
     * Highlight lines when a multi line comment is still 'open' (ie. matching end
     * delimiter has not yet been encountered).
     * 
     * @param content
     * 		the content to check
     * @param line
     * 		the line number
     * @return
     * 		true if there are comment lines before
     */
    protected boolean commentLinesBefore(String content, int line) {
      int offset = m_RootElement.getElement(line).getStartOffset();
  
      // Start of comment not found, nothing to do
  
      int startDelimiter = -1;
      if (getMultiLineComment())
        startDelimiter = lastIndexOf(content, getMultiLineCommentStart(), offset - 2);
  
      if (startDelimiter < 0)
        return false;
  
      // Matching start/end of comment found, nothing to do
  
      int endDelimiter = indexOf(content, getMultiLineCommentEnd(), startDelimiter);
  
      if (endDelimiter < offset & endDelimiter != -1)
        return false;
  
      // End of comment not found, highlight the lines
  
      m_Self.setCharacterAttributes(startDelimiter, offset - startDelimiter + 1,
  	DEFAULT_COMMENT, false);
      return true;
    }
  
    /**
     * Highlight comment lines to matching end delimiter.
     * 
     * @param content
     * 		the content to parse
     * @param line
     * 		the line number
     */
    protected void commentLinesAfter(String content, int line) {
      int offset = m_RootElement.getElement(line).getEndOffset();
  
      // End of comment not found, nothing to do
  
      int endDelimiter = -1;
      if (getMultiLineComment())
        endDelimiter = indexOf(content, getMultiLineCommentEnd(), offset);
  
      if (endDelimiter < 0)
        return;
  
      // Matching start/end of comment found, comment the lines
  
      int startDelimiter = lastIndexOf(content, getMultiLineCommentStart(), endDelimiter);
  
      if (startDelimiter < 0 || startDelimiter <= offset) {
        m_Self.setCharacterAttributes(offset, endDelimiter - offset + 1, DEFAULT_COMMENT,
  	  false);
      }
    }
  
    /**
     * Highlight lines to start or end delimiter.
     * 
     * @param content
     * 		the content to parse
     * @param line
     * 		the line number
     * @throws BadLocationException
     * 		if offsets are wrong
     */
    protected void highlightLinesAfter(String content, int line)
        throws BadLocationException {
      int offset = m_RootElement.getElement(line).getEndOffset();
  
      // Start/End delimiter not found, nothing to do
  
      int startDelimiter = -1;
      int endDelimiter = -1;
      if (getMultiLineComment()) {
        startDelimiter = indexOf(content, getMultiLineCommentStart(), offset);
        endDelimiter = indexOf(content, getMultiLineCommentEnd(), offset);
      }
  
      if (startDelimiter < 0)
        startDelimiter = content.length();
  
      if (endDelimiter < 0)
        endDelimiter = content.length();
  
      int delimiter = Math.min(startDelimiter, endDelimiter);
  
      if (delimiter < offset)
        return;
  
      // Start/End delimiter found, reapply highlighting
  
      int endLine = m_RootElement.getElementIndex(delimiter);
  
      for (int i = line + 1; i < endLine; i++) {
        Element branch = m_RootElement.getElement(i);
        Element leaf = m_Self.getCharacterElement(branch.getStartOffset());
        AttributeSet as = leaf.getAttributes();
  
        if (as.isEqual(DEFAULT_COMMENT))
  	applyHighlighting(content, i);
      }
    }
  
    /**
     * Parse the line to determine the appropriate highlighting.
     * 
     * @param content
     * 		the content to parse
     * @param line
     * 		the line number
     * @throws BadLocationException
     * 		if offsets are invalid
     */
    protected void applyHighlighting(String content, int line)
        throws BadLocationException {
      int startOffset = m_RootElement.getElement(line).getStartOffset();
      int endOffset = m_RootElement.getElement(line).getEndOffset() - 1;
  
      int lineLength = endOffset - startOffset;
      int contentLength = content.length();
  
      if (endOffset >= contentLength)
        endOffset = contentLength - 1;
  
      // check for multi line comments
      // (always set the comment attribute for the entire line)
  
      if (getMultiLineComment()) {
        if (endingMultiLineComment(content, startOffset, endOffset)
  	  || isMultiLineComment()
  	  || startingMultiLineComment(content, startOffset, endOffset)) {
  	m_Self.setCharacterAttributes(startOffset, endOffset - startOffset + 1,
  	    DEFAULT_COMMENT, false);
  	return;
        }
      }
  
      // set normal attributes for the line
  
      m_Self.setCharacterAttributes(startOffset, lineLength, DEFAULT_NORMAL, true);
  
      // check for single line comment
  
      int index = content.indexOf(getSingleLineCommentStart(), startOffset);
  
      if ((index > -1) && (index < endOffset)) {
        m_Self.setCharacterAttributes(index, endOffset - index + 1, DEFAULT_COMMENT, false);
        endOffset = index - 1;
      }
  
      // check for tokens
  
      checkForTokens(content, startOffset, endOffset);
    }
  
    /**
     * Does this line contain the start of a multi-line comment.
     * 
     * @param content
     * 		the content to search
     * @param startOffset
     * 		the start of the search
     * @param endOffset
     * 		the end of the search
     * @return
     * 		true if it contains the start delimiter
     * @throws BadLocationException
     * 		if offsets are invalid
     */
    protected boolean startingMultiLineComment(String content, int startOffset,
        int endOffset) throws BadLocationException {
      if (!getMultiLineComment())
        return false;
      
      int index = indexOf(content, getMultiLineCommentStart(), startOffset);
  
      if ((index < 0) || (index > endOffset))
        return false;
      else {
        setInsideMultiLineComment(true);
        return true;
      }
    }
  
    /**
     * Does this line contain the end delimiter of a multi-line comment.
     * 
     * @param content
     * 		the content to search
     * @param startOffset
     * 		the start of the search
     * @param endOffset
     * 		the end of the search
     * @return
     * 		true if the line contains the end delimiter
     * @throws BadLocationException
     * 		if offsets are invalid
     */
    protected boolean endingMultiLineComment(String content, int startOffset,
        int endOffset) throws BadLocationException {
      if (!getMultiLineComment())
        return false;
      
      int index = indexOf(content, getMultiLineCommentEnd(), startOffset);
  
      if ((index < 0) || (index > endOffset))
        return false;
      else {
        setInsideMultiLineComment(false);
        return true;
      }
    }
  
    /**
     * We have found a start delimiter and are still searching for the end
     * delimiter.
     * 
     * @return
     * 		true if currently within a multi-line comment
     */
    protected boolean isMultiLineComment() {
      return m_InsideMultiLineComment;
    }
  
    /**
     * Sets whether we're currently within a multi-line comment or not.
     * 
     * @param value
     * 		true if currently within a multi-line comment
     */
    protected void setInsideMultiLineComment(boolean value) {
      m_InsideMultiLineComment = value;
    }
  
    /**
     * Parse the line for tokens to highlight.
     * 
     * @param content
     * 		the content to parse
     * @param startOffset
     * 		the start position
     * @param endOffset
     * 		the end position
     */
    protected void checkForTokens(String content, int startOffset, int endOffset) {
      while (startOffset <= endOffset) {
        // skip the delimiters to find the start of a new token
  
        while (isDelimiter(content.substring(startOffset, startOffset + 1))) {
  	if (startOffset < endOffset)
  	  startOffset++;
  	else
  	  return;
        }
  
        // Extract and process the entire token
  
        if (isQuoteDelimiter(content.substring(startOffset, startOffset + 1)))
  	startOffset = getQuoteToken(content, startOffset, endOffset);
        else
  	startOffset = getOtherToken(content, startOffset, endOffset);
      }
    }
  
    /**
     * Searches for a quote token.
     *
     * @param content
     * 		the content to search
     * @param startOffset
     * 		the start of the search
     * @param endOffset
     * 		the end of the search
     * @return
     * 		the new position
     */
    protected int getQuoteToken(String content, int startOffset, int endOffset) {
      String quoteDelimiter = content.substring(startOffset, startOffset + 1);
      String escapeString = escapeQuote(quoteDelimiter);
  
      int index;
      int endOfQuote = startOffset;
  
      // skip over the escape quotes in this quote
  
      index = content.indexOf(escapeString, endOfQuote + 1);
  
      while ((index > -1) && (index < endOffset)) {
        endOfQuote = index + 1;
        index = content.indexOf(escapeString, endOfQuote);
      }
  
      // now find the matching delimiter
  
      index = content.indexOf(quoteDelimiter, endOfQuote + 1);
  
      if ((index < 0) || (index > endOffset))
        endOfQuote = endOffset;
      else
        endOfQuote = index;
  
      m_Self.setCharacterAttributes(startOffset, endOfQuote - startOffset + 1,
  	DEFAULT_STRING, false);
  
      return endOfQuote + 1;
    }
  
    /**
     * Searches for a keyword token.
     *
     * @param content
     * 		the content to search in
     * @param startOffset
     * 		the position to start the search fromm
     * @param endOffset
     * 		the position to end the search
     * @return
     * 		the new position
     */
    protected int getOtherToken(String content, int startOffset, int endOffset) {
      int endOfToken = startOffset + 1;
  
      while (endOfToken <= endOffset) {
        if (isDelimiter(content.substring(endOfToken, endOfToken + 1)))
  	break;
        endOfToken++;
      }
  
      String token = content.substring(startOffset, endOfToken);
  
      // see if this token has a highlighting format associated with it
      MutableAttributeSet attr = getKeywordFormatting(token);
      if (attr != null)
        m_Self.setCharacterAttributes(startOffset, endOfToken - startOffset, attr, false);
  
      return endOfToken + 1;
    }
  
    /**
     * Assume the needle will the found at the start/end of the line.
     * 
     * @param content
     * 		the content to search
     * @param needle
     * 		the string to look for
     * @param offset
     * 		the offset to start at
     * @return
     * 		the index
     */
    protected int indexOf(String content, String needle, int offset) {
      int index;
  
      while ((index = content.indexOf(needle, offset)) != -1) {
        String text = getLine(content, index).trim();
  
        if (text.startsWith(needle) || text.endsWith(needle))
  	break;
        else
  	offset = index + 1;
      }
  
      return index;
    }
  
    /**
     * Assume the needle will the found at the start/end of the line.
     * 
     * @param content
     * 		the content search
     * @param needle
     * 		what to look for
     * @param offset
     * 		the offset to start
     * @return
     * 		the index
     */
    protected int lastIndexOf(String content, String needle, int offset) {
      int index;
  
      while ((index = content.lastIndexOf(needle, offset)) != -1) {
        String text = getLine(content, index).trim();
  
        if (text.startsWith(needle) || text.endsWith(needle))
  	break;
        else
  	offset = index - 1;
      }
  
      return index;
    }
  
    /**
     * Returns the line.
     * 
     * @param content
     * 		the content
     * @param offset
     * 		the offset to start at
     * @return
     * 		the line
     */
    protected String getLine(String content, int offset) {
      int line = m_RootElement.getElementIndex(offset);
      Element lineElement = m_RootElement.getElement(line);
      int start = lineElement.getStartOffset();
      int end = lineElement.getEndOffset();
      return content.substring(start, end - 1);
    }
  
    /**
     * Checks whether the character is a delimiter.
     * 
     * @param character
     * 		the character to check
     * @return
     * 		true if a delimiter
     */
    public boolean isDelimiter(String character) {
      return Character.isWhitespace(character.charAt(0)) || (m_Delimiters.indexOf(character.charAt(0)) > -1);
    }
  
    /**
     * Checks whether the character is quote delimiter.
     * 
     * @param character
     * 		the character to check
     * @return
     * 		true if a quote delimiter
     */
    public boolean isQuoteDelimiter(String character) {
      return (m_QuoteDelimiters.indexOf(character.charAt(0)) > -1);
    }
  
    /**
     * Escapes the quote delimiter.
     * 
     * @param quoteDelimiter
     * 		the string to escape
     * @return
     * 		the escaped string
     */
    public String escapeQuote(String quoteDelimiter) {
      return m_QuoteEscape + quoteDelimiter;
    }
  
    /**
     * Adds the matching block end.
     * 
     * @param offset
     * 		the offset
     * @return
     * 		the string after adding the matching block end
     * @throws BadLocationException
     * 		if the offset is invalid
     */
    protected String addMatchingBlockEnd(int offset) throws BadLocationException {
      StringBuffer result;
      StringBuffer whiteSpace = new StringBuffer();
      int line = m_RootElement.getElementIndex(offset);
      int i = m_RootElement.getElement(line).getStartOffset();
  
      while (true) {
        String temp = m_Self.getText(i, 1);
  
        if (temp.equals(" ") || temp.equals("\t")) {
  	whiteSpace.append(temp);
  	i++;
        }
        else {
  	break;
        }
      }
  
      // assemble string
      result = new StringBuffer();
      result.append(m_BlockStart);
      result.append("\n");
      result.append(whiteSpace.toString());
      if (m_UseBlanks)
        result.append(m_Indentation);
      else
        result.append("\t");
      result.append("\n");
      result.append(whiteSpace.toString());
      result.append(m_BlockEnd);
      
      return result.toString();
    }
  
    /** 
     * gets the current font size.
     * 
     * @return
     * 		the font size
     */
    public int getFontSize() {
      return m_FontSize;
    }
  
    /** 
     * sets the current font size (affects all built-in styles).
     * 
     * @param fontSize
     * 		the size
     */
    public void setFontSize(int fontSize) {
      m_FontSize = fontSize;
      StyleConstants.setFontSize(DEFAULT_NORMAL, fontSize);
      StyleConstants.setFontSize(DEFAULT_STRING, fontSize);
      StyleConstants.setFontSize(DEFAULT_COMMENT, fontSize);
    }
  
    /**
     * gets the current font family.
     * 
     * @return
     * 		the font name
     */
    public String getFontName() {
      return m_FontName;
    }
  
    /**
     * sets the current font family (affects all built-in styles).
     * 
     * @param fontName
     * 		the font name
     */
    public void setFontName(String fontName) {
      m_FontName = fontName;
      StyleConstants.setFontFamily(DEFAULT_NORMAL, fontName);
      StyleConstants.setFontFamily(DEFAULT_STRING, fontName);
      StyleConstants.setFontFamily(DEFAULT_COMMENT, fontName);
    }
    
    /**
     * Sets the number of blanks to use for indentation.
     * 
     * @param value	
     * 		the number of blanks
     */
    public void setIndentationSize(int value) {
      int		i;
      
      m_Indentation = "";
      for (i = 0; i < value; i++)
        m_Indentation += " ";
    }
    
    /**
     * Returns the number of blanks used for indentation.
     * 
     * @return		
     * 		the number of blanks
     */
    public int getIndentationSize() {
      return m_Indentation.length();
    }
    
    /**
     * Sets the delimiter characters to use.
     * 
     * @param value	
     * 		the characters
     */
    public void setDelimiters(String value) {
      m_Delimiters = value;
    }
    
    /**
     * Returns the delimiter characters to use.
     * 
     * @return		
     * 		the characters
     */
    public String getDelimiters() {
      return m_Delimiters;
    }
    
    /**
     * Sets the quote delimiter characters to use.
     * 
     * @param value	
     * 		the characters
     */
    public void setQuoteDelimiters(String value) {
      m_QuoteDelimiters = value;
    }
    
    /**
     * Returns the quote delimiter characters to use.
     * 
     * @return		
     * 		the characters
     */
    public String getQuoteDelimiters() {
      return m_QuoteDelimiters;
    }
    
    /**
     * Sets the character to use for escaping a quote character.
     * 
     * @param value	
     * 		the character
     */
    public void setQuoteEscape(String value) {
      m_QuoteEscape = value;
    }
    
    /**
     * Returns the character for escaping a quote delimiter.
     * 
     * @return		
     * 		the character
     */
    public String getQuoteEscape() {
      return m_QuoteEscape;
    }
    
    /**
     * Sets the string that is the start of a single-line comment.
     * 
     * @param value	
     * 		the string
     */
    public void setSingleLineCommentStart(String value) {
      m_SingleLineCommentStart = value;
    }
  
    /**
     * Retrusn the single line comment start string.
     * 
     * @return
     * 		the start string
     */
    public String getSingleLineCommentStart() {
      return m_SingleLineCommentStart;
    }
    
    /**
     * Sets the string that is the start of a multi-line comment.
     * 
     * @param value	
     * 		the string
     */
    public void setMultiLineCommentStart(String value) {
      m_MultiLineCommentStart = value;
    }
    
    /**
     * Returns the string that is the start of a multi-line comment.
     * 
     * @return		
     * 		the string
     */
    public String getMultiLineCommentStart() {
      return m_MultiLineCommentStart;
    }
    
    /**
     * Sets the string that is the end of a multi-line comment.
     * 
     * @param value	the string
     */
    public void setMultiLineCommentEnd(String value) {
      m_MultiLineCommentEnd = value;
    }
  
    /**
     * Returns the end of a multi-line comment.
     * 
     * @return
     * 		the end string
     */
    public String getMultiLineCommentEnd() {
      return m_MultiLineCommentEnd;
    }
    
    /**
     * Sets the string that is the start of a block.
     * 
     * @param value
     * 		the string
     */
    public void setBlockStart(String value) {
      m_BlockStart = value;
    }
  
    /**
     * Returns the start of a block.
     * 
     * @return
     * 		the end string
     */
    public String getBlockStart() {
      return m_BlockStart;
    }
    
    /**
     * Sets the string that is the end of a block.
     * 
     * @param value	
     * 		the string
     */
    public void setBlockEnd(String value) {
      m_BlockEnd = value;
    }
  
    /**
     * Returns the end of a block.
     * 
     * @return
     * 		the end string
     */
    public String getBlockEnd() {
      return m_BlockEnd;
    }
    
    /**
     * Sets whether matching block ends are inserted or not.
     * 
     * @param value	
     * 		if true then matching block ends are inserted
     */
    public void setAddMatchingEndBlocks(boolean value) {
      m_AddMatchingEndBlocks = value;
    }
  
    /**
     * Returns whether matching block ends are inserted or not.
     * 
     * @return
     * 		true if matching block ends are inserted
     */
    public boolean getAddMatchingEndBlocks() {
      return m_AddMatchingEndBlocks;
    }
    
    /**
     * Sets whether to use blanks instead of tabs.
     * 
     * @param value	
     * 		if true then blanks are used instead of tabs
     */
    public void setUseBlanks(boolean value) {
      m_UseBlanks = value;
    }
  
    /**
     * Returns whether blanks are used instead of tabs.
     * 
     * @return
     * 		true if blanks are used instead of tabs
     */
    public boolean getUseBlanks() {
      return m_UseBlanks;
    }
    
    /**
     * Sets the background color.
     * 
     * @param value	
     * 		the background color
     */
    public void setBackgroundColor(Color value) {
      m_BackgroundColor = value;
    }
    
    /**
     * Returns the background color.
     * 
     * @return
     * 		the background color
     */
    public Color getBackgroundColor() {
      return m_BackgroundColor;
    }
    
    /**
     * Sets whether to enable multi-line comments.
     * 
     * @param value	
     * 		if true then multi-line comments are enabled
     */
    public void setMultiLineComment(boolean value) {
      m_MultiLineComment = value;
    }
  
    /**
     * Returns whether multi-line comments are enabled.
     * 
     * @return
     * 		true if multi-line comments are enabled
     */
    public boolean getMultiLineComment() {
      return m_MultiLineComment;
    }
    
    /**
     * Sets whether the keywords are case-sensitive or not.
     * 
     * @param value	
     * 		if true then keywords are treated case-sensitive
     */
    public void setCaseSensitive(boolean value) {
      m_CaseSensitive = value;
    }
  
    /**
     * Returns whether blanks are used instead of tabs.
     * 
     * @return
     * 		true if keywords are case-sensitive
     */
    public boolean getCaseSensitive() {
      return m_CaseSensitive;
    }
  }
  