org.fife.ui.rsyntaxtextarea.HtmlOccurrenceMarker Maven / Gradle / Ivy
/*
* 12/01/2014
*
* Copyright (C) 2013 Robert Futrell
* robert_futrell at users.sourceforge.net
* http://fifesoft.com/rsyntaxtextarea
*
* This library is distributed under a modified BSD license. See the included
* RSyntaxTextArea.License.txt file for details.
*/
package org.fife.ui.rsyntaxtextarea;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import org.fife.ui.rtextarea.SmartHighlightPainter;
/**
* Marks occurrences of the current token for HTML. Tags that require a
* closing tag have their "opposite" tag closed.
*
* @author Robert Futrell
* @version 1.0
*/
public class HtmlOccurrenceMarker implements OccurrenceMarker {
private static final char[] CLOSE_TAG_START = { '<', '/' };
private static final char[] TAG_SELF_CLOSE = { '/', '>' };
private static final Set TAGS_REQUIRING_CLOSING =
getRequiredClosingTags();
public static final Set getRequiredClosingTags() {
final String[] tags = {
"html",
"head",
"title",
"style",
"script",
"noscript",
"body",
"section",
"nav",
"article",
"aside",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"header",
"footer",
"address",
"pre",
"dialog",
"blockquote",
"ol",
"ul",
"dl",
"a",
"q",
"cite",
"em",
"strong",
"small",
"mark",
"dfn",
"abbr",
"time",
"progress",
"meter",
"code",
"var",
"samp",
"kbd",
"sub",
"sup",
"span",
"i",
"b",
"bdo",
"ruby",
"rt",
"rp",
"ins",
"del",
"figure",
"iframe",
"object",
"video",
"audio",
"canvas",
"map",
"table",
"caption",
"form",
"fieldset",
"label",
"button",
"select",
"datalist",
"textarea",
"output",
"details",
"bb",
"menu",
"legend",
"div",
// Obsolete elements
"acronym",
"applet",
"big",
"blink",
"center",
"dir",
"font",
"frame",
"frameset",
"isindex",
"listing",
"marquee",
"nobr",
"noembed",
"noframes",
"plaintext",
"s",
"spacer",
"strike",
"tt",
"u",
"xmp",
};
return new HashSet(Arrays.asList(tags));
}
/**
* If the caret is inside of a tag, this method returns the token
* representing the tag name; otherwise, null
is returned.
*
* Currently, this method only checks for tag names on the same line as
* the caret, for simplicity. In the future it could check prior lines
* until the tag name is found.
*
* @param textArea The text area.
* @param occurrenceMarker The occurrence marker.
* @return The token to mark occurrences of. Note that, if the
* specified occurrence marker identifies tokens other than
* tag names, these other element types may be returned.
*/
public static final Token getTagNameTokenForCaretOffset(
RSyntaxTextArea textArea, OccurrenceMarker occurrenceMarker) {
// Get the tag name token.
// For now, we only check for tags on the current line, for simplicity.
int dot = textArea.getCaretPosition();
Token t = textArea.getTokenListForLine(textArea.getCaretLineNumber());
Token toMark = null;
while (t!=null && t.isPaintable()) {
if (t.getType()==Token.MARKUP_TAG_NAME) {
toMark = t;
}
// Check for the token containing the caret before checking
// if it's the close token.
if (t.getEndOffset()==dot || t.containsPosition(dot)) {
// Some languages, like PHP, mark functions/variables (PHP,
// JavaScirpt) as well as HTML tags.
if (occurrenceMarker.isValidType(textArea, t) &&
t.getType()!=Token.MARKUP_TAG_NAME) {
return t;
}
if (t.containsPosition(dot)) {
break;
}
}
if (t.getType()==Token.MARKUP_TAG_DELIMITER) {
if (t.isSingleChar('>') || t.is(TAG_SELF_CLOSE)) {
toMark = null;
}
}
t = t.getNextToken();
}
return toMark;
}
/**
* {@inheritDoc}
*/
public Token getTokenToMark(RSyntaxTextArea textArea) {
return getTagNameTokenForCaretOffset(textArea, this);
}
/**
* {@inheritDoc}
*/
public boolean isValidType(RSyntaxTextArea textArea, Token t) {
return textArea.getMarkOccurrencesOfTokenType(t.getType());
}
/**
* {@inheritDoc}
*/
public void markOccurrences(RSyntaxDocument doc, Token t,
RSyntaxTextAreaHighlighter h, SmartHighlightPainter p) {
if (t.getType()!=Token.MARKUP_TAG_NAME) {
DefaultOccurrenceMarker.markOccurrencesOfToken(doc, t, h, p);
return;
}
String lexemeStr = t.getLexeme();
char[] lexeme = lexemeStr.toCharArray();
lexemeStr = lexemeStr.toLowerCase();
int tokenOffs = t.getOffset();
Element root = doc.getDefaultRootElement();
int lineCount = root.getElementCount();
int curLine = root.getElementIndex(t.getOffset());
int depth = 0;
// For now, we only check for tags on the current line, for
// simplicity. Tags spanning multiple lines aren't common anyway.
boolean found = false;
boolean forward = true;
t = doc.getTokenListForLine(curLine);
while (t!=null && t.isPaintable()) {
if (t.getType()==Token.MARKUP_TAG_DELIMITER) {
if (t.isSingleChar('<') && t.getOffset()+1==tokenOffs) {
// Don't try to match a tag that is optionally closed (or
// closing is forbidden entirely).
if (TAGS_REQUIRING_CLOSING.contains(lexemeStr)) {
found = true;
}
break;
}
else if (t.is(CLOSE_TAG_START) && t.getOffset()+2==tokenOffs) {
// Searching backward, we assume we can find the opening
// tag. Don't really care if it's valid or not.
found = true;
forward = false;
break;
}
}
t = t.getNextToken();
}
if (!found) {
return;
}
if (forward) {
t = t.getNextToken().getNextToken();
do {
while (t!=null && t.isPaintable()) {
if (t.getType()==Token.MARKUP_TAG_DELIMITER) {
if (t.is(CLOSE_TAG_START)) {
Token match = t.getNextToken();
if (match!=null && match.is(lexeme)) {
if (depth>0) {
depth--;
}
else {
try {
int end = match.getOffset() + match.length();
h.addMarkedOccurrenceHighlight(match.getOffset(), end, p);
end = tokenOffs + match.length();
h.addMarkedOccurrenceHighlight(tokenOffs, end, p);
} catch (BadLocationException ble) {
ble.printStackTrace(); // Never happens
}
return; // We're done!
}
}
}
else if (t.isSingleChar('<')) {
t = t.getNextToken();
if (t!=null && t.is(lexeme)) {
depth++;
}
}
}
t = t==null ? null : t.getNextToken();
}
if (++curLine openCloses = new ArrayList();
boolean inPossibleMatch = false;
t = doc.getTokenListForLine(curLine);
final int endBefore = tokenOffs - 2; // Stop before "".
do {
while (t!=null && t.getOffset()')) {
inPossibleMatch = false;
}
else if (inPossibleMatch && t.is(TAG_SELF_CLOSE)) {
openCloses.remove(openCloses.size()-1);
inPossibleMatch = false;
}
else if (t.is(CLOSE_TAG_START)) {
Token next = t.getNextToken();
if (next!=null) {
// Invalid XML might not have a match
if (next.is(lexeme)) {
openCloses.add(new Entry(false, next));
}
t = next;
}
}
}
t = t.getNextToken();
}
for (int i=openCloses.size()-1; i>=0; i--) {
Entry entry = openCloses.get(i);
depth += entry.open ? -1 : 1;
if (depth==-1) {
try {
Token match = entry.t;
int end = match.getOffset() + match.length();
h.addMarkedOccurrenceHighlight(match.getOffset(), end, p);
end = tokenOffs + match.length();
h.addMarkedOccurrenceHighlight(tokenOffs, end, p);
} catch (BadLocationException ble) {
ble.printStackTrace(); // Never happens
}
openCloses.clear();
return;
}
}
openCloses.clear();
if (--curLine>=0) {
t = doc.getTokenListForLine(curLine);
}
} while (curLine>=0);
}
}
/**
* Used internally when searching backward for a matching "open" tag.
*/
private static class Entry {
public boolean open;
public Token t;
public Entry(boolean open, Token t) {
this.open = open;
this.t = t;
}
}
}