All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fife.ui.rsyntaxtextarea.folding.HtmlFoldParser Maven / Gradle / Ivy

The newest version!
/*
 * 09/30/2012
 *
 * HtmlFoldParser.java - Fold parser for HTML 5 and PHP.
 * 
 * This library is distributed under a modified BSD license.  See the included
 * RSyntaxTextArea.License.txt file for details.
 */
package org.fife.ui.rsyntaxtextarea.folding;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import javax.swing.text.BadLocationException;

import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.Token;


/**
 * Fold parser for HTML 5, PHP and JSP.  For HTML, we currently don't fold
 * everything possible, just the "big" stuff.  For PHP, we only fold
 * the "big" HTML stuff and PHP regions, not code blocks in the actual PHP.
 * For JSP we only fold the "big" HTML stuff and JSP blocks, not anything in
 * the actual Java code.
 *
 * @author Robert Futrell
 * @version 1.0
 */
public class HtmlFoldParser implements FoldParser {

	/**
	 * Constant denoting we're folding HTML.
	 */
	public static final int LANGUAGE_HTML	= -1;

	/**
	 * Constant denoting we're folding PHP.
	 */
	public static final int LANGUAGE_PHP	= 0;

	/**
	 * Constant denoting we're folding JSP.
	 */
	public static final int LANGUAGE_JSP	= 1;

	/**
	 * The language we're folding.
	 */
	private final int language;

	/**
	 * The set of tags we allow to be folded.  These are tags that must have
	 * explicit close tags in both HTML 4 and HTML 5.
	 */
	private static final Set FOLDABLE_TAGS;

	private static final char[] MARKUP_CLOSING_TAG_START = "".toCharArray();
	private static final char[] MLC_START = "".toCharArray();

	private static final char[] PHP_START = "".toCharArray();

	// Scriptlets, declarations, and expressions all start the same way.
	private static final char[] JSP_START = "<%".toCharArray();
	private static final char[] JSP_END = "%>".toCharArray();

	private static final char[][] LANG_START = { PHP_START, JSP_START };
	private static final char[][] LANG_END   = { PHP_END, JSP_END };

	private static final char[] JSP_COMMENT_START = "<%--".toCharArray();
	private static final char[] JSP_COMMENT_END   = "--%>".toCharArray();

	static {
		FOLDABLE_TAGS = new HashSet();
		FOLDABLE_TAGS.add("body");
		FOLDABLE_TAGS.add("canvas");
		FOLDABLE_TAGS.add("div");
		FOLDABLE_TAGS.add("form");
		FOLDABLE_TAGS.add("head");
		FOLDABLE_TAGS.add("html");
		FOLDABLE_TAGS.add("ol");
		FOLDABLE_TAGS.add("pre");
		FOLDABLE_TAGS.add("script");
		FOLDABLE_TAGS.add("span");
		FOLDABLE_TAGS.add("style");
		FOLDABLE_TAGS.add("table");
		FOLDABLE_TAGS.add("tfoot");
		FOLDABLE_TAGS.add("thead");
		FOLDABLE_TAGS.add("tr");
		FOLDABLE_TAGS.add("td");
		FOLDABLE_TAGS.add("ul");
	}


	/**
	 * Constructor.
	 *
	 * @param language The language to fold, such as {@link #LANGUAGE_PHP}.
	 */
	public HtmlFoldParser(int language) {
		if (languageLANGUAGE_JSP) {
			throw new IllegalArgumentException("Invalid language: " + language);
		}
		this.language = language;
	}


	/**
	 * {@inheritDoc}
	 */
	public List getFolds(RSyntaxTextArea textArea) {

		List folds = new ArrayList();
		Stack tagNameStack = new Stack();
		boolean inSublanguage = false;

		Fold currentFold = null;
		int lineCount = textArea.getLineCount();
		boolean inMLC = false;
		boolean inJSMLC = false;
		TagCloseInfo tci = new TagCloseInfo();

		try {

			for (int line=0; line=0 && t.getType()==Token.SEPARATOR) {

						//  or %>
						else if (t.startsWith(LANG_END[language])) {
							int phpEnd = t.getEndOffset() - 1;
							currentFold.setEndOffset(phpEnd);
							Fold parentFold = currentFold.getParent();
							// Don't add fold markers for single-line blocks
							if (currentFold.isOnSingleLine()) {
								removeFold(currentFold, folds);
							}
							currentFold = parentFold;
							inSublanguage = false;
							t = t.getNextToken();
							continue;
						}

					}

					if (!inSublanguage) {

						if (t.getType()==Token.COMMENT_MULTILINE) {

							// Continuing an MLC from a previous line
							if (inMLC) {
								// Found the end of the MLC starting on a previous line...
								if (t.endsWith(MLC_END)) {
									int mlcEnd = t.getEndOffset() - 1;
									currentFold.setEndOffset(mlcEnd);
									Fold parentFold = currentFold.getParent();
									// Don't add fold markers for single-line blocks
									if (currentFold.isOnSingleLine()) {
										removeFold(currentFold, folds);
									}
									currentFold = parentFold;
									inMLC = false;
								}
								// Otherwise, this MLC is continuing on to yet
								// another line.
							}
	
							// Continuing a JS MLC from a previous line
							else if (inJSMLC) {
								// Found the end of the MLC starting on a previous line...
								if (t.endsWith(JSP_COMMENT_END)) {
									int mlcEnd = t.getEndOffset() - 1;
									currentFold.setEndOffset(mlcEnd);
									Fold parentFold = currentFold.getParent();
									// Don't add fold markers for single-line blocks
									if (currentFold.isOnSingleLine()) {
										removeFold(currentFold, folds);
									}
									currentFold = parentFold;
									inJSMLC = false;
								}
								// Otherwise, this MLC is continuing on to yet
								// another line.
							}

							// Starting a MLC that ends on a later line...
							else if (t.startsWith(MLC_START) && !t.endsWith(MLC_END)) {
								if (currentFold==null) {
									currentFold = new Fold(FoldType.COMMENT, textArea, t.getOffset());
									folds.add(currentFold);
								}
								else {
									currentFold = currentFold.createChild(FoldType.COMMENT, t.getOffset());
								}
								inMLC = true;
							}

							// Starting a JSP comment that ends on a later line...
							else if (language==LANGUAGE_JSP &&
									t.startsWith(JSP_COMMENT_START) &&
									!t.endsWith(JSP_COMMENT_END)) {
								if (currentFold==null) {
									currentFold = new Fold(FoldType.COMMENT, textArea, t.getOffset());
									folds.add(currentFold);
								}
								else {
									currentFold = currentFold.createChild(FoldType.COMMENT, t.getOffset());
								}
								inJSMLC = true;
							}

						}

						// If we're starting a new tag...
						else if (t.isSingleChar(Token.MARKUP_TAG_DELIMITER, '<')) {
							Token tagStartToken = t;
							Token tagNameToken = t.getNextToken();
							if (isFoldableTag(tagNameToken)) {
								getTagCloseInfo(tagNameToken, textArea, line, tci);
								if (tci.line==-1) { // EOF reached before end of tag
									return folds;
								}
								// We have found either ">" or "/>" with tci.
								Token tagCloseToken = tci.closeToken;
								if (tagCloseToken.isSingleChar(Token.MARKUP_TAG_DELIMITER, '>')) {
									if (currentFold==null) {
										currentFold = new Fold(FoldType.CODE, textArea, tagStartToken.getOffset());
										folds.add(currentFold);
									}
									else {
										currentFold = currentFold.createChild(FoldType.CODE, tagStartToken.getOffset());
									}
									tagNameStack.push(tagNameToken.getLexeme());
								}
								t = tagCloseToken; // Continue parsing after tag
							}
						}

						// If we've found a closing tag (e.g. "
"). else if (t.is(Token.MARKUP_TAG_DELIMITER, MARKUP_CLOSING_TAG_START)) { if (currentFold!=null) { Token tagNameToken = t.getNextToken(); if (isFoldableTag(tagNameToken) && isEndOfLastFold(tagNameStack, tagNameToken)) { tagNameStack.pop(); currentFold.setEndOffset(t.getOffset()); Fold parentFold = currentFold.getParent(); // Don't add fold markers for single-line blocks if (currentFold.isOnSingleLine()) { removeFold(currentFold, folds); } currentFold = parentFold; t = tagNameToken; } } } } t = t.getNextToken(); } } } catch (BadLocationException ble) { // Should never happen ble.printStackTrace(); } return folds; } /** * Grabs the token representing the closing of a tag (i.e. * ">" or "/>"). This should only be * called after a tag name has been parsed, to ensure the "closing" of * other tags is not identified. * * @param tagNameToken The token denoting the name of the tag. * @param textArea The text area whose contents are being parsed. * @param line The line we're currently on. * @param info On return, information about the closing of the tag is * returned in this object. */ private void getTagCloseInfo(Token tagNameToken, RSyntaxTextArea textArea, int line, TagCloseInfo info) { info.reset(); Token t = tagNameToken.getNextToken(); do { while (t!=null && t.getType()!=Token.MARKUP_TAG_DELIMITER) { t = t.getNextToken(); } if (t!=null) { info.closeToken = t; info.line = line; break; } } while (++line</...>") with a * specific name is the closing tag of our current fold region. * * @param tagNameStack The stack of fold regions. * @param tagNameToken The tag name of the most recently parsed closing * tag. * @return Whether it's the end of the current fold region. */ private static final boolean isEndOfLastFold(Stack tagNameStack, Token tagNameToken) { if (tagNameToken!=null && !tagNameStack.isEmpty()) { return tagNameToken.getLexeme().equalsIgnoreCase(tagNameStack.peek()); } return false; } /** * Returns whether a tag is one we allow as a foldable region. * * @param tagNameToken The tag's name token. This may be null. * @return Whether this tag can be a foldable region. */ private static final boolean isFoldableTag(Token tagNameToken) { return tagNameToken!=null && FOLDABLE_TAGS.contains(tagNameToken.getLexeme().toLowerCase()); } /** * If this fold has a parent fold, this method removes it from its parent. * Otherwise, it's assumed to be the most recent (top-level) fold in the * folds list, and is removed from that. * * @param fold The fold to remove. * @param folds The list of top-level folds. */ private static final void removeFold(Fold fold, List folds) { if (!fold.removeFromParent()) { folds.remove(folds.size()-1); } } /** * A simple wrapper for the token denoting the closing of a tag (i.e. * ">" or "/>"). */ private static class TagCloseInfo { private Token closeToken; private int line; public void reset() { closeToken = null; line = -1; } @Override public String toString() { return "[TagCloseInfo: " + "closeToken=" + closeToken + ", line=" + line + "]"; } } }