Mega Code Archive

 
Categories / Android / Development
 

A character queue with parsing helpers

/**  * A character queue with parsing helpers.  *  * @author Jonathan Hedley  */ public class TokenQueue {     private String queue;     private int pos = 0;          private static final char ESC = '\\'; // escape char for chomp balanced.     /**      Create a new TokenQueue.      @param data string of data to back queue.      */     public TokenQueue(String data) {         queue = data;     }     /**      * Is the queue empty?      * @return true if no data left in queue.      */     public boolean isEmpty() {         return remainingLength() == 0;     }          private int remainingLength() {         return queue.length() - pos;     }     /**      * Retrieves but does not remove the first character from the queue.      * @return First character, or 0 if empty.      */     public char peek() {         return isEmpty() ? 0 : queue.charAt(pos);     }     /**      Add a character to the start of the queue (will be the next character retrieved).      @param c character to add      */     public void addFirst(Character c) {         addFirst(c.toString());     }     /**      Add a string to the start of the queue.      @param seq string to add.      */     public void addFirst(String seq) {         // not very performant, but an edge case         queue = seq + queue.substring(pos);         pos = 0;     }     /**      * Tests if the next characters on the queue match the sequence. Case insensitive.      * @param seq String to check queue for.      * @return true if the next characters match.      */     public boolean matches(String seq) {         return queue.regionMatches(true, pos, seq, 0, seq.length());     }     /**      * Case sensitive match test.      * @param seq      * @return      */     public boolean matchesCS(String seq) {         return queue.startsWith(seq, pos);     }          /**      Tests if the next characters match any of the sequences. Case insensitive.      @param seq      @return      */     public boolean matchesAny(String... seq) {         for (String s : seq) {             if (matches(s))                 return true;         }         return false;     }     public boolean matchesAny(char... seq) {         if (isEmpty())             return false;         for (char c: seq) {             if (queue.charAt(pos) == c)                 return true;         }         return false;     }     public boolean matchesStartTag() {         // micro opt for matching "<x"         return (remainingLength() >= 2 && queue.charAt(pos) == '<' && Character.isLetter(queue.charAt(pos+1)));     }     /**      * Tests if the queue matches the sequence (as with match), and if they do, removes the matched string from the      * queue.      * @param seq String to search for, and if found, remove from queue.      * @return true if found and removed, false if not found.      */     public boolean matchChomp(String seq) {         if (matches(seq)) {             pos += seq.length();             return true;         } else {             return false;         }     }     /**      Tests if queue starts with a whitespace character.      @return if starts with whitespace      */     public boolean matchesWhitespace() {         return !isEmpty() && Character.isWhitespace(queue.charAt(pos));     }     /**      Test if the queue matches a word character (letter or digit).      @return if matches a word character      */     public boolean matchesWord() {         return !isEmpty() && Character.isLetterOrDigit(queue.charAt(pos));     }     /**      * Drops the next character off the queue.      */     public void advance() {         if (!isEmpty()) pos++;     }     /**      * Consume one character off queue.      * @return first character on queue.      */     public char consume() {         return queue.charAt(pos++);     }     /**      * Consumes the supplied sequence of the queue. If the queue does not start with the supplied sequence, will      * throw an illegal state exception -- but you should be running match() against that condition.      <p>      Case insensitive.      * @param seq sequence to remove from head of queue.      */     public void consume(String seq) {         if (!matches(seq))             throw new IllegalStateException("Queue did not match expected sequence");         int len = seq.length();         if (len > remainingLength())             throw new IllegalStateException("Queue not long enough to consume sequence");                  pos += len;     }     /**      * Pulls a string off the queue, up to but exclusive of the match sequence, or to the queue running out.      * @param seq String to end on (and not include in return, but leave on queue). <b>Case sensitive.</b>      * @return The matched data consumed from queue.      */     public String consumeTo(String seq) {         int offset = queue.indexOf(seq, pos);         if (offset != -1) {             String consumed = queue.substring(pos, offset);             pos += consumed.length();             return consumed;         } else {             return remainder();         }     }          public String consumeToIgnoreCase(String seq) {         int start = pos;         String first = seq.substring(0, 1);         boolean canScan = first.toLowerCase().equals(first.toUpperCase()); // if first is not cased, use index of         while (!isEmpty()) {             if (matches(seq))                 break;                          if (canScan) {                 int skip = queue.indexOf(first, pos) - pos;                 if (skip == 0) // this char is the skip char, but not match, so force advance of pos                     pos++;                 else if (skip < 0) // no chance of finding, grab to end                     pos = queue.length();                 else                     pos += skip;             }             else                 pos++;         }         String data = queue.substring(start, pos);          return data;      }     /**      Consumes to the first sequence provided, or to the end of the queue. Leaves the terminator on the queue.      @param seq any number of terminators to consume to. <b>Case insensitive.</b>      @return consumed string         */     // todo: method name. not good that consumeTo cares for case, and consume to any doesn't. And the only use for this     // is is a case sensitive time...     public String consumeToAny(String... seq) {         int start = pos;         while (!isEmpty() && !matchesAny(seq)) {             pos++;         }         String data = queue.substring(start, pos);          return data;      }     /**      * Pulls a string off the queue (like consumeTo), and then pulls off the matched string (but does not return it).      * <p>      * If the queue runs out of characters before finding the seq, will return as much as it can (and queue will go      * isEmpty() == true).      * @param seq String to match up to, and not include in return, and to pull off queue. <b>Case sensitive.</b>      * @return Data matched from queue.      */     public String chompTo(String seq) {         String data = consumeTo(seq);         matchChomp(seq);         return data;     }          public String chompToIgnoreCase(String seq) {         String data = consumeToIgnoreCase(seq); // case insensitive scan         matchChomp(seq);         return data;     }     /**      * Pulls a balanced string off the queue. E.g. if queue is "(one (two) three) four", (,) will return "one (two) three",      * and leave " four" on the queue. Unbalanced openers and closers can be escaped (with \). Those escapes will be left      * in the returned string, which is suitable for regexes (where we need to preserve the escape), but unsuitable for      * contains text strings; use unescape for that.      * @param open opener      * @param close closer      * @return data matched from the queue      */     public String chompBalanced(char open, char close) {         StringBuilder accum = new StringBuilder();         int depth = 0;         char last = 0;         do {             if (isEmpty()) break;             Character c = consume();             if (last == 0 || last != ESC) {                 if (c.equals(open))                     depth++;                 else if (c.equals(close))                     depth--;             }             if (depth > 0 && last != 0)                 accum.append(c); // don't include the outer match pair in the return             last = c;         } while (depth > 0);         return accum.toString();     }          /**      * Unescaped a \ escaped string.      * @param in backslash escaped string      * @return unescaped string      */     public static String unescape(String in) {         StringBuilder out = new StringBuilder();         char last = 0;         for (char c : in.toCharArray()) {             if (c == ESC) {                 if (last != 0 && last == ESC)                     out.append(c);             }             else                  out.append(c);             last = c;         }         return out.toString();     }     /**      * Pulls the next run of whitespace characters of the queue.      */     public boolean consumeWhitespace() {         boolean seen = false;         while (matchesWhitespace()) {             pos++;             seen = true;         }         return seen;     }     /**      * Retrieves the next run of word type (letter or digit) off the queue.      * @return String of word characters from queue, or empty string if none.      */     public String consumeWord() {         int start = pos;         while (matchesWord())             pos++;         return queue.substring(start, pos);     }          /**      * Consume an tag name off the queue (word or :, _, -)      *       * @return tag name      */     public String consumeTagName() {         int start = pos;         while (!isEmpty() && (matchesWord() || matchesAny(':', '_', '-')))             pos++;                  return queue.substring(start, pos);     }          /**      * Consume a CSS element selector (tag name, but | instead of : for namespaces, to not conflict with :pseudo selects).      *       * @return tag name      */     public String consumeElementSelector() {         int start = pos;         while (!isEmpty() && (matchesWord() || matchesAny('|', '_', '-')))             pos++;                  return queue.substring(start, pos);     }     /**      Consume a CSS identifier (ID or class) off the queue (letter, digit, -, _)      http://www.w3.org/TR/CSS2/syndata.html#value-def-identifier      @return identifier      */     public String consumeCssIdentifier() {         int start = pos;         while (!isEmpty() && (matchesWord() || matchesAny('-', '_')))             pos++;         return queue.substring(start, pos);     }     /**      Consume an attribute key off the queue (letter, digit, -, _, :")      @return attribute key      */     public String consumeAttributeKey() {         int start = pos;         while (!isEmpty() && (matchesWord() || matchesAny('-', '_', ':')))             pos++;                  return queue.substring(start, pos);     }     /**      Consume and return whatever is left on the queue.      @return remained of queue.      */     public String remainder() {         StringBuilder accum = new StringBuilder();         while (!isEmpty()) {             accum.append(consume());         }         return accum.toString();     }          public String toString() {         return queue.substring(pos);     } }