package chatty.util.commands;
import java.text.ParseException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Custom Commands parser. Returns an Items object containing all the elements
* of the Custom Command.
*
* @author tduva
*/
public class Parser {
private final String input;
private final StringReader reader;
public Parser(String text) {
input = text;
reader = new StringReader(text);
}
/**
* Parses a Custom Command.
*
* @return
* @throws ParseException
*/
Items parse() throws ParseException {
return parse(null);
}
/**
* Reads in values until it encounters one of the characters defined in the
* given regex parameter. It won't include the character that made it stop
* in the result.
*
* @param to Regex that will make it stop (single-character)
* @return An Items object containing all the parsed elements
* @throws ParseException If the parser encountered something unexpected
*/
private Items parse(String to) throws ParseException {
Items items = new Items();
while (reader.hasNext() && (to == null || !reader.peek().matches(to))) {
if (accept("$")) {
items.add(specialThing());
} else if (accept("\\")) {
if (reader.hasNext()) {
// Just read next character as literal
items.add(reader.next());
}
} else {
items.add(reader.next());
}
}
items.flush();
return items;
}
/**
* If the parser encountered something unexpected, this will create an error
* message and throw a ParseException.
*
* @param message
* @throws ParseException
*/
private void error(String message) throws ParseException {
int pos = reader.pos() + 1;
String errorPos = input.substring(0, pos)+"<"+pos+">"+input.substring(pos, input.length());
throw new ParseException(message+" at pos <"+pos+"> ["+errorPos+"]", reader.pos());
}
/**
* A single character that the parser can accept as next character, but
* won't throw an error if it's not there. If the character is indeed there
* it will be read (advancing the read index), but not returned.
*
* @param character A single character
* @return true if the character is the next character, false otherwise
*/
private boolean accept(String character) {
if (reader.hasNext() && reader.peek().equals(character)) {
reader.next();
return true;
}
return false;
}
private boolean acceptMatch(String regex) {
if (reader.hasNext() && reader.peek().matches(regex)) {
reader.next();
return true;
}
return false;
}
/**
* A single character that the parser expects to be the next character, and
* will throw an error if it's not there. This will advance the read index.
*
* @param character A single character
* @throws ParseException
*/
private void expect(String character) throws ParseException {
if (!reader.hasNext() || !reader.next().equals(character)) {
error("Expected "+character);
}
}
/**
* Parse stuff that occurs after a '$'.
*
* @return
* @throws ParseException
*/
private Item specialThing() throws ParseException {
boolean isRequired = false;
if (accept("$")) {
isRequired = true;
}
String type = functionName();
if (type.isEmpty()) {
return replacement(isRequired);
}
else if (type.equals("if")) {
return condition(isRequired);
}
else if (type.equals("join")) {
return join(isRequired);
}
else if (type.equals("ifeq")) {
return ifEq(isRequired);
}
else {
error("Invalid function '"+type+"'");
return null;
}
}
/**
* $(chan) $(3) $(5-) $3 $3-
*
* @return
*/
private Item identifier() throws ParseException {
String ref = read("[a-zA-Z0-9-_]");
Matcher m = Pattern.compile("([0-9])+(-)?").matcher(ref);
if (ref.isEmpty()) {
error("Expected identifier");
}
if (m.matches()) {
int index = Integer.parseInt(m.group(1));
if (index == 0) {
error("Invalid index 0");
}
boolean toEnd = m.group(2) != null;
return new RangeIdentifier(index, toEnd);
} else {
return new Identifier(ref);
}
}
private Item tinyIdentifier() throws ParseException {
String ref = read("[0-9]");
if (ref.isEmpty()) {
error("Expected number");
}
int index = Integer.parseInt(ref);
if (index == 0) {
error("Invalid index 0");
}
boolean toEnd = false;
if (accept("-")) {
toEnd = true;
}
return new RangeIdentifier(index, toEnd);
}
private Item condition(boolean isRequired) throws ParseException {
expect("(");
Item identifier = identifier();
expect(",");
Items output1 = param();
Items output2 = null;
if (accept(",")) {
output2 = lastParam();
}
expect(")");
return new If(identifier, isRequired, output1, output2);
}
private Item ifEq(boolean isRequired) throws ParseException {
expect("(");
Item identifier = identifier();
expect(",");
Items compare = param();
expect(",");
Items output1 = param();
Items output2 = null;
if (accept(",")) {
output2 = lastParam();
}
expect(")");
return new IfEq(identifier, isRequired, compare, output1, output2);
}
private Item join(boolean isRequired) throws ParseException {
expect("(");
Item identifier = identifier();
expect(",");
Items separator = parse("[)]");
if (separator.isEmpty()) {
error("Expected separator string");
}
expect(")");
return new Join(identifier, separator, isRequired);
}
private Replacement replacement(boolean isRequired) throws ParseException {
if (accept("(")) {
Item identifier = identifier();
expect(")");
return new Replacement(identifier, isRequired);
}
else {
Item identifier = tinyIdentifier();
return new Replacement(identifier, isRequired);
}
}
private String functionName() {
return read("[a-zA-Z]");
}
private Items param() throws ParseException {
return parse("[,)]");
}
private Items lastParam() throws ParseException {
return parse("[)]");
}
/**
* Read until it encounters a character matching the given regex. The
* character the caused it to stop won't be read.
*
* @param regex A regex matching a single character
* @return The read String
*/
private String read(String regex) {
StringBuilder b = new StringBuilder();
while (reader.hasNext() && reader.peek().matches(regex)) {
b.append(reader.next());
}
return b.toString();
}
public static void main(String[] args) {
Identifier id = new Identifier("abc");
Literal lit = new Literal("abcd");
Items items = new Items();
items.add(id);
items.add(new Identifier("aijofwe"));
items.add("_ffweffabc");
items.add(new Join(new Identifier("cheese"), null, true));
System.out.println(Item.getIdentifiersWithPrefix("_", id, lit, items));
}
}