// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.resource.text.modes;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.TreeSet;
import javax.swing.text.Segment;
import org.fife.ui.rsyntaxtextarea.AbstractTokenMaker;
import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities;
import org.fife.ui.rsyntaxtextarea.Token;
import org.fife.ui.rsyntaxtextarea.TokenMap;
import org.infinity.resource.ResourceFactory;
import org.infinity.util.IdsMap;
import org.infinity.util.IdsMapCache;
import org.infinity.util.IdsMapEntry;
/**
* A token maker that turns text into a linked list of {@code Token}s
* for syntax highlighting Infinity Engine BCS scripts.
*/
public class BCSTokenMaker extends AbstractTokenMaker
{
/** Style for highlighting BCS scripts. */
public static final String SYNTAX_STYLE_BCS = "text/BCS";
// available token types
public static final int TOKEN_IDENTIFIER = Token.IDENTIFIER; // used for unrecognized literals
public static final int TOKEN_KEYWORD = Token.RESERVED_WORD;
public static final int TOKEN_ACTION = Token.FUNCTION;
public static final int TOKEN_TRIGGER = Token.DATA_TYPE;
public static final int TOKEN_OBJECT = Token.VARIABLE;
public static final int TOKEN_NUMBER = Token.LITERAL_NUMBER_DECIMAL_INT;
public static final int TOKEN_HEXNUMBER = Token.LITERAL_NUMBER_HEXADECIMAL;
public static final int TOKEN_STRING = Token.LITERAL_STRING_DOUBLE_QUOTE;
public static final int TOKEN_COMMENT_LINE = Token.COMMENT_EOL;
public static final int TOKEN_COMMENT_BLOCK = Token.COMMENT_MULTILINE;
public static final int TOKEN_SYMBOL = Token.MARKUP_TAG_NAME;
public static final int TOKEN_SYMBOL_SPELL = Token.MARKUP_TAG_ATTRIBUTE;
public static final int TOKEN_OPERATOR = Token.OPERATOR;
public static final int TOKEN_WHITESPACE = Token.WHITESPACE;
private static final String CharWhiteSpace = " \t";
private static final String CharOperator = "!|,.()[]";
private static final String CharHexPrefix = "xX";
private int currentTokenStart;
private int currentTokenType;
public BCSTokenMaker()
{
super();
}
@Override
public TokenMap getWordsToHighlight()
{
TokenMap tokenMap = new TokenMap();
IdsMap map;
// symbolic names
List<String> idsFile = createIdsList();
for (Iterator<String> iterIDS = idsFile.iterator(); iterIDS.hasNext();) {
String ids = iterIDS.next();
int type = ("SPELL.IDS".equalsIgnoreCase(ids)) ? TOKEN_SYMBOL_SPELL : TOKEN_SYMBOL;
if (ResourceFactory.resourceExists(ids)) {
map = IdsMapCache.get(ids);
if (map != null) {
for (Iterator<IdsMapEntry> iterEntry = map.getAllValues().iterator(); iterEntry.hasNext();) {
String name = iterEntry.next().getString();
if (name != null && !name.isEmpty()) {
tokenMap.put(name, type);
}
}
}
}
}
tokenMap.put("ANYONE", TOKEN_SYMBOL);
// objects
map = IdsMapCache.get("OBJECT.IDS");
for (Iterator<IdsMapEntry> iter = map.getAllValues().iterator(); iter.hasNext();) {
String name = extractFunctionName(iter.next().getString());
if (name != null && !name.isEmpty()) {
tokenMap.put(name, TOKEN_OBJECT);
}
}
// actions
map = IdsMapCache.get("ACTION.IDS");
for (Iterator<IdsMapEntry> iter = map.getAllValues().iterator(); iter.hasNext();) {
String name = extractFunctionName(iter.next().getString());
if (name != null && !name.isEmpty()) {
tokenMap.put(name, TOKEN_ACTION);
}
}
// triggers
map = IdsMapCache.get("TRIGGER.IDS");
for (Iterator<IdsMapEntry> iter = map.getAllValues().iterator(); iter.hasNext();) {
String name = extractFunctionName(iter.next().getString());
if (name != null && !name.isEmpty()) {
tokenMap.put(name, TOKEN_TRIGGER);
}
}
// keywords
tokenMap.put("IF", TOKEN_KEYWORD);
tokenMap.put("THEN", TOKEN_KEYWORD);
tokenMap.put("RESPONSE", TOKEN_KEYWORD);
tokenMap.put("END", TOKEN_KEYWORD);
return tokenMap;
}
@Override
public void addToken(Segment segment, int start, int end, int tokenType, int startOffset)
{
if (tokenType == TOKEN_IDENTIFIER) {
int value = wordsToHighlight.get(segment, start, end);
if (value != -1) {
tokenType = value;
}
}
super.addToken(segment, start, end, tokenType, startOffset);
}
@Override
public String[] getLineCommentStartAndEnd(int languageIndex)
{
return new String[]{"// ", null};
}
@Override
public boolean getMarkOccurrencesOfTokenType(int type)
{
return type == TOKEN_ACTION || type == TOKEN_TRIGGER ||
type == TOKEN_OBJECT || type == TOKEN_SYMBOL || type == TOKEN_SYMBOL_SPELL;
}
@Override
public boolean getShouldIndentNextLineAfter(Token token)
{
// if (token != null) {
// if (token.getType() == TOKEN_KEYWORD) {
// String s = String.valueOf(token.getTextArray(), token.getTextOffset(),
// token.getEndOffset() - token.getTextOffset());
// if ("IF".equals(s) || "THEN".equals(s) || s.startsWith("#")) {
// return true;
// }
// }
// }
return false;
}
/**
* Returns a list of tokens representing the given text.
*
* @param text The text to break into tokens.
* @param initialTokenType The token with which to start tokenizing.
* @param startOffset The offset at which the line of tokens begins.
* @return A linked list of tokens representing {@code text}.
*/
@Override
public Token getTokenList(Segment text, int initialTokenType, int startOffset)
{
resetTokenList();
char[] array = text.array;
int ofs = text.offset;
int cnt = text.count;
int end = text.offset + cnt;
int newStartOfs = startOffset - ofs;
currentTokenStart = ofs;
currentTokenType = initialTokenType;
boolean tokenCheckComment = false; // indicates whether character is part of the comment prefix/suffix
for (int i = ofs; i < end; i++) {
char c = array[i];
switch (currentTokenType) {
case Token.NULL:
{
currentTokenStart = i; // starting new token here
if (c == '#') {
// keyword: action block probability
if (i+1 < end && RSyntaxUtilities.isDigit(array[i+1])) {
currentTokenType = TOKEN_KEYWORD;
} else {
currentTokenType = TOKEN_IDENTIFIER;
}
} else if (c == '/') {
// comment
currentTokenType = TOKEN_IDENTIFIER;
if (i+1 < end) {
if (array[i+1] == '/') {
tokenCheckComment = true;
currentTokenType = TOKEN_COMMENT_LINE;
} else if (array[i+1] == '*') {
tokenCheckComment = true;
currentTokenType = TOKEN_COMMENT_BLOCK;
}
}
} else if (c == '"') {
// string
currentTokenType = TOKEN_STRING;
} else if (CharWhiteSpace.indexOf(c) > -1) {
// whitespace
currentTokenType = TOKEN_WHITESPACE;
} else if (CharOperator.indexOf(c) > -1) {
// operator
currentTokenType = TOKEN_OPERATOR;
} else {
if (c == '-') {
if (i+1 < end && RSyntaxUtilities.isDigit(array[i+1])) {
// negative number
currentTokenType = TOKEN_NUMBER;
if (array[i+1] == '0' && i+2 < end && CharHexPrefix.indexOf(array[i+2]) > -1) {
// hex number
currentTokenType = TOKEN_HEXNUMBER;
}
}
} else if (RSyntaxUtilities.isDigit(c)) {
currentTokenType = TOKEN_NUMBER;
if (c == '0' && i+1 < end && CharHexPrefix.indexOf(array[i+1]) > -1) {
// hex number
currentTokenType = TOKEN_HEXNUMBER;
}
} else {
// a potential identifier
currentTokenType = TOKEN_IDENTIFIER;
}
}
} // end of case Token.NULL:
break;
case TOKEN_KEYWORD:
{
if (RSyntaxUtilities.isDigit(c)) {
// still keyword
} else if (c == '/') {
// comment
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_IDENTIFIER;
if (i+1 < end) {
if (array[i+1] == '/') {
tokenCheckComment = true;
currentTokenType = TOKEN_COMMENT_LINE;
} else if (array[i+1] == '*') {
tokenCheckComment = true;
currentTokenType = TOKEN_COMMENT_BLOCK;
}
}
} else if (c == '"') {
// string
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_STRING;
} else if (CharWhiteSpace.indexOf(c) > -1) {
// whitespace
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_WHITESPACE;
} else if (CharOperator.indexOf(c) > -1) {
// operator
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_OPERATOR;
} else {
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
if (c == '-') {
if (i+1 < end && RSyntaxUtilities.isDigit(array[i+1])) {
// negative number
currentTokenType = TOKEN_NUMBER;
if (array[i+1] == '0' && i+2 < end && CharHexPrefix.indexOf(array[i+2]) > -1) {
// hex number
currentTokenType = TOKEN_HEXNUMBER;
}
}
} else {
// a potential identifier
currentTokenType = TOKEN_IDENTIFIER;
}
}
} // end of case TOKEN_KEYWORD:
break;
case TOKEN_OPERATOR:
{
if (c == '#') {
// keyword: start of action block probability
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
if (i+1 < end && RSyntaxUtilities.isDigit(array[i+1])) {
currentTokenType = TOKEN_KEYWORD;
} else {
currentTokenType = TOKEN_IDENTIFIER;
}
} else if (c == '/') {
// comment
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_IDENTIFIER;
if (i+1 < end) {
if (array[i+1] == '/') {
tokenCheckComment = true;
currentTokenType = TOKEN_COMMENT_LINE;
} else if (array[i+1] == '*') {
tokenCheckComment = true;
currentTokenType = TOKEN_COMMENT_BLOCK;
}
}
} else if (c == '"') {
// string
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_STRING;
} else if (CharWhiteSpace.indexOf(c) > -1) {
// whitespace
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_WHITESPACE;
} else if (CharOperator.indexOf(c) > -1) {
// still operator
} else {
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
if (c == '-') {
if (i+1 < end && RSyntaxUtilities.isDigit(array[i+1])) {
// negative number
currentTokenType = TOKEN_NUMBER;
if (array[i+1] == '0' && i+2 < end && CharHexPrefix.indexOf(array[i+2]) > -1) {
// hex number
currentTokenType = TOKEN_HEXNUMBER;
}
}
} else if (RSyntaxUtilities.isDigit(c)) {
currentTokenType = TOKEN_NUMBER;
if (c == '0' && i+1 < end && CharHexPrefix.indexOf(array[i+1]) > -1) {
// hex number
currentTokenType = TOKEN_HEXNUMBER;
}
} else {
// a potential identifier
currentTokenType = TOKEN_IDENTIFIER;
}
}
} // end of case TOKEN_OPERATOR:
break;
case TOKEN_WHITESPACE:
{
if (c == '#') {
// keyword: start of action block probability
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
if (i+1 < end && RSyntaxUtilities.isDigit(array[i+1])) {
currentTokenType = TOKEN_KEYWORD;
} else {
currentTokenType = TOKEN_IDENTIFIER;
}
} else if (c == '/') {
// comment
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_IDENTIFIER;
if (i+1 < end) {
if (array[i+1] == '/') {
tokenCheckComment = true;
currentTokenType = TOKEN_COMMENT_LINE;
} else if (array[i+1] == '*') {
tokenCheckComment = true;
currentTokenType = TOKEN_COMMENT_BLOCK;
}
}
} else if (c == '"') {
// string
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_STRING;
} else if (CharWhiteSpace.indexOf(c) > -1) {
// still whitespace
} else if (CharOperator.indexOf(c) > -1) {
// operator
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_OPERATOR;
} else {
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
if (c == '-') {
if (i+1 < end && RSyntaxUtilities.isDigit(array[i+1])) {
// negative number
currentTokenType = TOKEN_NUMBER;
if (array[i+1] == '0' && i+2 < end && CharHexPrefix.indexOf(array[i+2]) > -1) {
// hex number
currentTokenType = TOKEN_HEXNUMBER;
}
}
} else if (RSyntaxUtilities.isDigit(c)) {
currentTokenType = TOKEN_NUMBER;
if (c == '0' && i+1 < end && CharHexPrefix.indexOf(array[i+1]) > -1) {
// hex number
currentTokenType = TOKEN_HEXNUMBER;
}
} else {
// a potential identifier
currentTokenType = TOKEN_IDENTIFIER;
}
}
} // end of case TOKEN_WHITESPACE:
break;
case TOKEN_COMMENT_LINE:
// still line comment
break;
case TOKEN_COMMENT_BLOCK:
{
if (tokenCheckComment) {
if (c == '/') {
addToken(text, currentTokenStart, i, currentTokenType, newStartOfs+currentTokenStart);
currentTokenType = Token.NULL;
}
tokenCheckComment = false;
} else if (c == '*') {
if (i+1 < end && array[i+1] == '/') {
tokenCheckComment = true;
}
}
} // end of case TOKEN_COMMENT_BLOCK:
break;
case TOKEN_STRING:
{
if (c == '"') {
addToken(text, currentTokenStart, i, currentTokenType, newStartOfs+currentTokenStart);
currentTokenType = Token.NULL;
}
} // end of case TOKEN_STRING:
break;
case TOKEN_HEXNUMBER:
case TOKEN_NUMBER:
{
if (c == '#') {
// keyword: start of action block probability
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
if (i+1 < end && RSyntaxUtilities.isDigit(array[i+1])) {
currentTokenType = TOKEN_KEYWORD;
} else {
currentTokenType = TOKEN_IDENTIFIER;
}
} else if (c == '/') {
// comment
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_IDENTIFIER;
if (i+1 < end) {
if (array[i+1] == '/') {
tokenCheckComment = true;
currentTokenType = TOKEN_COMMENT_LINE;
} else if (array[i+1] == '*') {
tokenCheckComment = true;
currentTokenType = TOKEN_COMMENT_BLOCK;
}
}
} else if (c == '"') {
// string
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_STRING;
} else if (CharWhiteSpace.indexOf(c) > -1) {
// whitespace
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_WHITESPACE;
} else if (CharOperator.indexOf(c) > -1) {
// operator
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_OPERATOR;
} else if (currentTokenType == TOKEN_HEXNUMBER && (RSyntaxUtilities.isHexCharacter(c) || CharHexPrefix.indexOf(c) > -1)) {
// still a hex number?
if (CharHexPrefix.indexOf(c) > -1 && (i == currentTokenStart || array[i-1] != '0')) {
currentTokenType = TOKEN_IDENTIFIER;
}
} else if (currentTokenType == TOKEN_NUMBER && RSyntaxUtilities.isDigit(c)) {
// still a number
} else {
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
// a potential identifier
currentTokenType = TOKEN_IDENTIFIER;
}
} // end of case TOKEN_NUMBER:
break;
case TOKEN_IDENTIFIER:
{
if (c == '/') {
// comment
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_IDENTIFIER;
if (i+1 < end) {
if (array[i+1] == '/') {
tokenCheckComment = true;
currentTokenType = TOKEN_COMMENT_LINE;
} else if (array[i+1] == '*') {
tokenCheckComment = true;
currentTokenType = TOKEN_COMMENT_BLOCK;
}
}
} else if (c == '"') {
// string
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_STRING;
} else if (CharWhiteSpace.indexOf(c) > -1) {
// whitespace
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = Token.WHITESPACE;
} else if (CharOperator.indexOf(c) > -1) {
// operator
addToken(text, currentTokenStart, i-1, currentTokenType, newStartOfs+currentTokenStart);
currentTokenStart = i;
currentTokenType = TOKEN_OPERATOR;
} else {
// still identifier
}
} // end of case TOKEN_IDENTIFIER:
break;
default: // should never happen
{
try {
throw new Exception(String.format("Invalid token %1$d found at position %2$d",
currentTokenType, newStartOfs+i));
} catch (Exception e) {
e.printStackTrace();
}
} // end of default:
} // end of switch (currentTokenType)
} // end of for (int i = ofs; i < end; i++)
// adding the current token to the list
switch (currentTokenType) {
case Token.NULL: // do nothing if no token is active
addNullToken();
break;
case TOKEN_COMMENT_BLOCK: // block comments can span multiple lines
addToken(text, currentTokenStart, end-1, currentTokenType, newStartOfs+currentTokenStart);
break;
default: // everything else doesn't continue to the next line
addToken(text, currentTokenStart, end-1, currentTokenType, newStartOfs+currentTokenStart);
addNullToken();
}
return firstToken;
}
// Extracts the function name of the action/trigger definition
private String extractFunctionName(String function)
{
if (function != null && !function.isEmpty()) {
int idx = function.indexOf('(');
if (idx < 0) {
idx = function.length();
}
if (idx > 0) {
return function.substring(0, idx).trim();
}
}
return null;
}
// Scans action and trigger definitions for referenced IDS files and returns them as a sorted list
private List<String> createIdsList()
{
final String[] files = { "ACTION.IDS", "TRIGGER.IDS" };
TreeSet<String> idsSet = new TreeSet<>();
for (final String idsFile: files) {
IdsMap map = IdsMapCache.get(idsFile);
if (map != null) {
for (final IdsMapEntry entry: map.getMap().values()) {
String[] params = entry.getParameters().split(",");
if (params != null) {
for (final String param: params) {
int p = param.lastIndexOf('*');
if (p >= 0 && p+1 < param.length()) {
String ids = param.substring(p+1).trim().toUpperCase(Locale.ENGLISH);
if (ids.length() > 0) {
ids += ".IDS";
if (ResourceFactory.resourceExists(ids)) {
idsSet.add(ids);
}
}
}
}
}
}
}
}
List<String> retVal = new ArrayList<>();
for (final String s: idsSet) {
retVal.add(s);
}
return retVal;
}
}