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)); } }