--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/share/classes/javax/swing/text/html/CSSParser.java Sat Dec 01 00:00:00 2007 +0000
@@ -0,0 +1,854 @@
+/*
+ * Copyright 1999-2000 Sun Microsystems, Inc. All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Sun designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Sun in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+package javax.swing.text.html;
+
+import java.io.*;
+
+/**
+ * A CSS parser. This works by way of a delegate that implements the
+ * CSSParserCallback interface. The delegate is notified of the following
+ * events:
+ * <ul>
+ * <li>Import statement: <code>handleImport</code>
+ * <li>Selectors <code>handleSelector</code>. This is invoked for each
+ * string. For example if the Reader contained p, bar , a {}, the delegate
+ * would be notified 4 times, for 'p,' 'bar' ',' and 'a'.
+ * <li>When a rule starts, <code>startRule</code>
+ * <li>Properties in the rule via the <code>handleProperty</code>. This
+ * is invoked one per property/value key, eg font size: foo;, would
+ * cause the delegate to be notified once with a value of 'font size'.
+ * <li>Values in the rule via the <code>handleValue</code>, this is notified
+ * for the total value.
+ * <li>When a rule ends, <code>endRule</code>
+ * </ul>
+ * This will parse much more than CSS 1, and loosely implements the
+ * recommendation for <i>Forward-compatible parsing</i> in section
+ * 7.1 of the CSS spec found at:
+ * <a href=http://www.w3.org/TR/REC-CSS1>http://www.w3.org/TR/REC-CSS1</a>.
+ * If an error results in parsing, a RuntimeException will be thrown.
+ * <p>
+ * This will preserve case. If the callback wishes to treat certain poritions
+ * case insensitively (such as selectors), it should use toLowerCase, or
+ * something similar.
+ *
+ * @author Scott Violet
+ */
+class CSSParser {
+ // Parsing something like the following:
+ // (@rule | ruleset | block)*
+ //
+ // @rule (block | identifier)*; (block with {} ends @rule)
+ // block matching [] () {} (that is, [()] is a block, [(){}{[]}]
+ // is a block, ()[] is two blocks)
+ // identifier "*" | '*' | anything but a [](){} and whitespace
+ //
+ // ruleset selector decblock
+ // selector (identifier | (block, except block '{}') )*
+ // declblock declaration* block*
+ // declaration (identifier* stopping when identifier ends with :)
+ // (identifier* stopping when identifier ends with ;)
+ //
+ // comments /* */ can appear any where, and are stripped.
+
+
+ // identifier - letters, digits, dashes and escaped characters
+ // block starts with { ends with matching }, () [] and {} always occur
+ // in matching pairs, '' and "" also occur in pairs, except " may be
+
+
+ // Indicates the type of token being parsed.
+ private static final int IDENTIFIER = 1;
+ private static final int BRACKET_OPEN = 2;
+ private static final int BRACKET_CLOSE = 3;
+ private static final int BRACE_OPEN = 4;
+ private static final int BRACE_CLOSE = 5;
+ private static final int PAREN_OPEN = 6;
+ private static final int PAREN_CLOSE = 7;
+ private static final int END = -1;
+
+ private static final char[] charMapping = { 0, 0, '[', ']', '{', '}', '(',
+ ')', 0};
+
+
+ /** Set to true if one character has been read ahead. */
+ private boolean didPushChar;
+ /** The read ahead character. */
+ private int pushedChar;
+ /** Temporary place to hold identifiers. */
+ private StringBuffer unitBuffer;
+ /** Used to indicate blocks. */
+ private int[] unitStack;
+ /** Number of valid blocks. */
+ private int stackCount;
+ /** Holds the incoming CSS rules. */
+ private Reader reader;
+ /** Set to true when the first non @ rule is encountered. */
+ private boolean encounteredRuleSet;
+ /** Notified of state. */
+ private CSSParserCallback callback;
+ /** nextToken() inserts the string here. */
+ private char[] tokenBuffer;
+ /** Current number of chars in tokenBufferLength. */
+ private int tokenBufferLength;
+ /** Set to true if any whitespace is read. */
+ private boolean readWS;
+
+
+ // The delegate interface.
+ static interface CSSParserCallback {
+ /** Called when an @import is encountered. */
+ void handleImport(String importString);
+ // There is currently no way to distinguish between '"foo,"' and
+ // 'foo,'. But this generally isn't valid CSS. If it becomes
+ // a problem, handleSelector will have to be told if the string is
+ // quoted.
+ void handleSelector(String selector);
+ void startRule();
+ // Property names are mapped to lower case before being passed to
+ // the delegate.
+ void handleProperty(String property);
+ void handleValue(String value);
+ void endRule();
+ }
+
+ CSSParser() {
+ unitStack = new int[2];
+ tokenBuffer = new char[80];
+ unitBuffer = new StringBuffer();
+ }
+
+ void parse(Reader reader, CSSParserCallback callback,
+ boolean inRule) throws IOException {
+ this.callback = callback;
+ stackCount = tokenBufferLength = 0;
+ this.reader = reader;
+ encounteredRuleSet = false;
+ try {
+ if (inRule) {
+ parseDeclarationBlock();
+ }
+ else {
+ while (getNextStatement());
+ }
+ } finally {
+ callback = null;
+ reader = null;
+ }
+ }
+
+ /**
+ * Gets the next statement, returning false if the end is reached. A
+ * statement is either an @rule, or a ruleset.
+ */
+ private boolean getNextStatement() throws IOException {
+ unitBuffer.setLength(0);
+
+ int token = nextToken((char)0);
+
+ switch (token) {
+ case IDENTIFIER:
+ if (tokenBufferLength > 0) {
+ if (tokenBuffer[0] == '@') {
+ parseAtRule();
+ }
+ else {
+ encounteredRuleSet = true;
+ parseRuleSet();
+ }
+ }
+ return true;
+ case BRACKET_OPEN:
+ case BRACE_OPEN:
+ case PAREN_OPEN:
+ parseTillClosed(token);
+ return true;
+
+ case BRACKET_CLOSE:
+ case BRACE_CLOSE:
+ case PAREN_CLOSE:
+ // Shouldn't happen...
+ throw new RuntimeException("Unexpected top level block close");
+
+ case END:
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Parses an @ rule, stopping at a matching brace pair, or ;.
+ */
+ private void parseAtRule() throws IOException {
+ // PENDING: make this more effecient.
+ boolean done = false;
+ boolean isImport = (tokenBufferLength == 7 &&
+ tokenBuffer[0] == '@' && tokenBuffer[1] == 'i' &&
+ tokenBuffer[2] == 'm' && tokenBuffer[3] == 'p' &&
+ tokenBuffer[4] == 'o' && tokenBuffer[5] == 'r' &&
+ tokenBuffer[6] == 't');
+
+ unitBuffer.setLength(0);
+ while (!done) {
+ int nextToken = nextToken(';');
+
+ switch (nextToken) {
+ case IDENTIFIER:
+ if (tokenBufferLength > 0 &&
+ tokenBuffer[tokenBufferLength - 1] == ';') {
+ --tokenBufferLength;
+ done = true;
+ }
+ if (tokenBufferLength > 0) {
+ if (unitBuffer.length() > 0 && readWS) {
+ unitBuffer.append(' ');
+ }
+ unitBuffer.append(tokenBuffer, 0, tokenBufferLength);
+ }
+ break;
+
+ case BRACE_OPEN:
+ if (unitBuffer.length() > 0 && readWS) {
+ unitBuffer.append(' ');
+ }
+ unitBuffer.append(charMapping[nextToken]);
+ parseTillClosed(nextToken);
+ done = true;
+ // Skip a tailing ';', not really to spec.
+ {
+ int nextChar = readWS();
+ if (nextChar != -1 && nextChar != ';') {
+ pushChar(nextChar);
+ }
+ }
+ break;
+
+ case BRACKET_OPEN: case PAREN_OPEN:
+ unitBuffer.append(charMapping[nextToken]);
+ parseTillClosed(nextToken);
+ break;
+
+ case BRACKET_CLOSE: case BRACE_CLOSE: case PAREN_CLOSE:
+ throw new RuntimeException("Unexpected close in @ rule");
+
+ case END:
+ done = true;
+ break;
+ }
+ }
+ if (isImport && !encounteredRuleSet) {
+ callback.handleImport(unitBuffer.toString());
+ }
+ }
+
+ /**
+ * Parses the next rule set, which is a selector followed by a
+ * declaration block.
+ */
+ private void parseRuleSet() throws IOException {
+ if (parseSelectors()) {
+ callback.startRule();
+ parseDeclarationBlock();
+ callback.endRule();
+ }
+ }
+
+ /**
+ * Parses a set of selectors, returning false if the end of the stream
+ * is reached.
+ */
+ private boolean parseSelectors() throws IOException {
+ // Parse the selectors
+ int nextToken;
+
+ if (tokenBufferLength > 0) {
+ callback.handleSelector(new String(tokenBuffer, 0,
+ tokenBufferLength));
+ }
+
+ unitBuffer.setLength(0);
+ for (;;) {
+ while ((nextToken = nextToken((char)0)) == IDENTIFIER) {
+ if (tokenBufferLength > 0) {
+ callback.handleSelector(new String(tokenBuffer, 0,
+ tokenBufferLength));
+ }
+ }
+ switch (nextToken) {
+ case BRACE_OPEN:
+ return true;
+
+ case BRACKET_OPEN: case PAREN_OPEN:
+ parseTillClosed(nextToken);
+ // Not too sure about this, how we handle this isn't very
+ // well spec'd.
+ unitBuffer.setLength(0);
+ break;
+
+ case BRACKET_CLOSE: case BRACE_CLOSE: case PAREN_CLOSE:
+ throw new RuntimeException("Unexpected block close in selector");
+
+ case END:
+ // Prematurely hit end.
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Parses a declaration block. Which a number of declarations followed
+ * by a })].
+ */
+ private void parseDeclarationBlock() throws IOException {
+ for (;;) {
+ int token = parseDeclaration();
+ switch (token) {
+ case END: case BRACE_CLOSE:
+ return;
+
+ case BRACKET_CLOSE: case PAREN_CLOSE:
+ // Bail
+ throw new RuntimeException("Unexpected close in declaration block");
+ case IDENTIFIER:
+ break;
+ }
+ }
+ }
+
+ /**
+ * Parses a single declaration, which is an identifier a : and another
+ * identifier. This returns the last token seen.
+ */
+ // identifier+: identifier* ;|}
+ private int parseDeclaration() throws IOException {
+ int token;
+
+ if ((token = parseIdentifiers(':', false)) != IDENTIFIER) {
+ return token;
+ }
+ // Make the property name to lowercase
+ for (int counter = unitBuffer.length() - 1; counter >= 0; counter--) {
+ unitBuffer.setCharAt(counter, Character.toLowerCase
+ (unitBuffer.charAt(counter)));
+ }
+ callback.handleProperty(unitBuffer.toString());
+
+ token = parseIdentifiers(';', true);
+ callback.handleValue(unitBuffer.toString());
+ return token;
+ }
+
+ /**
+ * Parses identifiers until <code>extraChar</code> is encountered,
+ * returning the ending token, which will be IDENTIFIER if extraChar
+ * is found.
+ */
+ private int parseIdentifiers(char extraChar,
+ boolean wantsBlocks) throws IOException {
+ int nextToken;
+ int ubl;
+
+ unitBuffer.setLength(0);
+ for (;;) {
+ nextToken = nextToken(extraChar);
+
+ switch (nextToken) {
+ case IDENTIFIER:
+ if (tokenBufferLength > 0) {
+ if (tokenBuffer[tokenBufferLength - 1] == extraChar) {
+ if (--tokenBufferLength > 0) {
+ if (readWS && unitBuffer.length() > 0) {
+ unitBuffer.append(' ');
+ }
+ unitBuffer.append(tokenBuffer, 0,
+ tokenBufferLength);
+ }
+ return IDENTIFIER;
+ }
+ if (readWS && unitBuffer.length() > 0) {
+ unitBuffer.append(' ');
+ }
+ unitBuffer.append(tokenBuffer, 0, tokenBufferLength);
+ }
+ break;
+
+ case BRACKET_OPEN:
+ case BRACE_OPEN:
+ case PAREN_OPEN:
+ ubl = unitBuffer.length();
+ if (wantsBlocks) {
+ unitBuffer.append(charMapping[nextToken]);
+ }
+ parseTillClosed(nextToken);
+ if (!wantsBlocks) {
+ unitBuffer.setLength(ubl);
+ }
+ break;
+
+ case BRACE_CLOSE:
+ // No need to throw for these two, we return token and
+ // caller can do whatever.
+ case BRACKET_CLOSE:
+ case PAREN_CLOSE:
+ case END:
+ // Hit the end
+ return nextToken;
+ }
+ }
+ }
+
+ /**
+ * Parses till a matching block close is encountered. This is only
+ * appropriate to be called at the top level (no nesting).
+ */
+ private void parseTillClosed(int openToken) throws IOException {
+ int nextToken;
+ boolean done = false;
+
+ startBlock(openToken);
+ while (!done) {
+ nextToken = nextToken((char)0);
+ switch (nextToken) {
+ case IDENTIFIER:
+ if (unitBuffer.length() > 0 && readWS) {
+ unitBuffer.append(' ');
+ }
+ if (tokenBufferLength > 0) {
+ unitBuffer.append(tokenBuffer, 0, tokenBufferLength);
+ }
+ break;
+
+ case BRACKET_OPEN: case BRACE_OPEN: case PAREN_OPEN:
+ if (unitBuffer.length() > 0 && readWS) {
+ unitBuffer.append(' ');
+ }
+ unitBuffer.append(charMapping[nextToken]);
+ startBlock(nextToken);
+ break;
+
+ case BRACKET_CLOSE: case BRACE_CLOSE: case PAREN_CLOSE:
+ if (unitBuffer.length() > 0 && readWS) {
+ unitBuffer.append(' ');
+ }
+ unitBuffer.append(charMapping[nextToken]);
+ endBlock(nextToken);
+ if (!inBlock()) {
+ done = true;
+ }
+ break;
+
+ case END:
+ // Prematurely hit end.
+ throw new RuntimeException("Unclosed block");
+ }
+ }
+ }
+
+ /**
+ * Fetches the next token.
+ */
+ private int nextToken(char idChar) throws IOException {
+ readWS = false;
+
+ int nextChar = readWS();
+
+ switch (nextChar) {
+ case '\'':
+ readTill('\'');
+ if (tokenBufferLength > 0) {
+ tokenBufferLength--;
+ }
+ return IDENTIFIER;
+ case '"':
+ readTill('"');
+ if (tokenBufferLength > 0) {
+ tokenBufferLength--;
+ }
+ return IDENTIFIER;
+ case '[':
+ return BRACKET_OPEN;
+ case ']':
+ return BRACKET_CLOSE;
+ case '{':
+ return BRACE_OPEN;
+ case '}':
+ return BRACE_CLOSE;
+ case '(':
+ return PAREN_OPEN;
+ case ')':
+ return PAREN_CLOSE;
+ case -1:
+ return END;
+ default:
+ pushChar(nextChar);
+ getIdentifier(idChar);
+ return IDENTIFIER;
+ }
+ }
+
+ /**
+ * Gets an identifier, returning true if the length of the string is greater than 0,
+ * stopping when <code>stopChar</code>, whitespace, or one of {}()[] is
+ * hit.
+ */
+ // NOTE: this could be combined with readTill, as they contain somewhat
+ // similiar functionality.
+ private boolean getIdentifier(char stopChar) throws IOException {
+ boolean lastWasEscape = false;
+ boolean done = false;
+ int escapeCount = 0;
+ int escapeChar = 0;
+ int nextChar;
+ int intStopChar = (int)stopChar;
+ // 1 for '\', 2 for valid escape char [0-9a-fA-F], 3 for
+ // stop character (white space, ()[]{}) 0 otherwise
+ short type;
+ int escapeOffset = 0;
+
+ tokenBufferLength = 0;
+ while (!done) {
+ nextChar = readChar();
+ switch (nextChar) {
+ case '\\':
+ type = 1;
+ break;
+
+ case '0': case '1': case '2': case '3': case '4': case '5':
+ case '6': case '7': case '8': case '9':
+ type = 2;
+ escapeOffset = nextChar - '0';
+ break;
+
+ case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+ type = 2;
+ escapeOffset = nextChar - 'a' + 10;
+ break;
+
+ case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+ type = 2;
+ escapeOffset = nextChar - 'A' + 10;
+ break;
+
+ case '\'': case '"': case '[': case ']': case '{': case '}':
+ case '(': case ')':
+ case ' ': case '\n': case '\t': case '\r':
+ type = 3;
+ break;
+
+ case '/':
+ type = 4;
+ break;
+
+ case -1:
+ // Reached the end
+ done = true;
+ type = 0;
+ break;
+
+ default:
+ type = 0;
+ break;
+ }
+ if (lastWasEscape) {
+ if (type == 2) {
+ // Continue with escape.
+ escapeChar = escapeChar * 16 + escapeOffset;
+ if (++escapeCount == 4) {
+ lastWasEscape = false;
+ append((char)escapeChar);
+ }
+ }
+ else {
+ // no longer escaped
+ lastWasEscape = false;
+ if (escapeCount > 0) {
+ append((char)escapeChar);
+ // Make this simpler, reprocess the character.
+ pushChar(nextChar);
+ }
+ else if (!done) {
+ append((char)nextChar);
+ }
+ }
+ }
+ else if (!done) {
+ if (type == 1) {
+ lastWasEscape = true;
+ escapeChar = escapeCount = 0;
+ }
+ else if (type == 3) {
+ done = true;
+ pushChar(nextChar);
+ }
+ else if (type == 4) {
+ // Potential comment
+ nextChar = readChar();
+ if (nextChar == '*') {
+ done = true;
+ readComment();
+ readWS = true;
+ }
+ else {
+ append('/');
+ if (nextChar == -1) {
+ done = true;
+ }
+ else {
+ pushChar(nextChar);
+ }
+ }
+ }
+ else {
+ append((char)nextChar);
+ if (nextChar == intStopChar) {
+ done = true;
+ }
+ }
+ }
+ }
+ return (tokenBufferLength > 0);
+ }
+
+ /**
+ * Reads till a <code>stopChar</code> is encountered, escaping characters
+ * as necessary.
+ */
+ private void readTill(char stopChar) throws IOException {
+ boolean lastWasEscape = false;
+ int escapeCount = 0;
+ int escapeChar = 0;
+ int nextChar;
+ boolean done = false;
+ int intStopChar = (int)stopChar;
+ // 1 for '\', 2 for valid escape char [0-9a-fA-F], 0 otherwise
+ short type;
+ int escapeOffset = 0;
+
+ tokenBufferLength = 0;
+ while (!done) {
+ nextChar = readChar();
+ switch (nextChar) {
+ case '\\':
+ type = 1;
+ break;
+
+ case '0': case '1': case '2': case '3': case '4':case '5':
+ case '6': case '7': case '8': case '9':
+ type = 2;
+ escapeOffset = nextChar - '0';
+ break;
+
+ case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+ type = 2;
+ escapeOffset = nextChar - 'a' + 10;
+ break;
+
+ case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+ type = 2;
+ escapeOffset = nextChar - 'A' + 10;
+ break;
+
+ case -1:
+ // Prematurely reached the end!
+ throw new RuntimeException("Unclosed " + stopChar);
+
+ default:
+ type = 0;
+ break;
+ }
+ if (lastWasEscape) {
+ if (type == 2) {
+ // Continue with escape.
+ escapeChar = escapeChar * 16 + escapeOffset;
+ if (++escapeCount == 4) {
+ lastWasEscape = false;
+ append((char)escapeChar);
+ }
+ }
+ else {
+ // no longer escaped
+ if (escapeCount > 0) {
+ append((char)escapeChar);
+ if (type == 1) {
+ lastWasEscape = true;
+ escapeChar = escapeCount = 0;
+ }
+ else {
+ if (nextChar == intStopChar) {
+ done = true;
+ }
+ append((char)nextChar);
+ lastWasEscape = false;
+ }
+ }
+ else {
+ append((char)nextChar);
+ lastWasEscape = false;
+ }
+ }
+ }
+ else if (type == 1) {
+ lastWasEscape = true;
+ escapeChar = escapeCount = 0;
+ }
+ else {
+ if (nextChar == intStopChar) {
+ done = true;
+ }
+ append((char)nextChar);
+ }
+ }
+ }
+
+ private void append(char character) {
+ if (tokenBufferLength == tokenBuffer.length) {
+ char[] newBuffer = new char[tokenBuffer.length * 2];
+ System.arraycopy(tokenBuffer, 0, newBuffer, 0, tokenBuffer.length);
+ tokenBuffer = newBuffer;
+ }
+ tokenBuffer[tokenBufferLength++] = character;
+ }
+
+ /**
+ * Parses a comment block.
+ */
+ private void readComment() throws IOException {
+ int nextChar;
+
+ for(;;) {
+ nextChar = readChar();
+ switch (nextChar) {
+ case -1:
+ throw new RuntimeException("Unclosed comment");
+ case '*':
+ nextChar = readChar();
+ if (nextChar == '/') {
+ return;
+ }
+ else if (nextChar == -1) {
+ throw new RuntimeException("Unclosed comment");
+ }
+ else {
+ pushChar(nextChar);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ /**
+ * Called when a block start is encountered ({[.
+ */
+ private void startBlock(int startToken) {
+ if (stackCount == unitStack.length) {
+ int[] newUS = new int[stackCount * 2];
+
+ System.arraycopy(unitStack, 0, newUS, 0, stackCount);
+ unitStack = newUS;
+ }
+ unitStack[stackCount++] = startToken;
+ }
+
+ /**
+ * Called when an end block is encountered )]}
+ */
+ private void endBlock(int endToken) {
+ int startToken;
+
+ switch (endToken) {
+ case BRACKET_CLOSE:
+ startToken = BRACKET_OPEN;
+ break;
+ case BRACE_CLOSE:
+ startToken = BRACE_OPEN;
+ break;
+ case PAREN_CLOSE:
+ startToken = PAREN_OPEN;
+ break;
+ default:
+ // Will never happen.
+ startToken = -1;
+ break;
+ }
+ if (stackCount > 0 && unitStack[stackCount - 1] == startToken) {
+ stackCount--;
+ }
+ else {
+ // Invalid state, should do something.
+ throw new RuntimeException("Unmatched block");
+ }
+ }
+
+ /**
+ * @return true if currently in a block.
+ */
+ private boolean inBlock() {
+ return (stackCount > 0);
+ }
+
+ /**
+ * Skips any white space, returning the character after the white space.
+ */
+ private int readWS() throws IOException {
+ int nextChar;
+ while ((nextChar = readChar()) != -1 &&
+ Character.isWhitespace((char)nextChar)) {
+ readWS = true;
+ }
+ return nextChar;
+ }
+
+ /**
+ * Reads a character from the stream.
+ */
+ private int readChar() throws IOException {
+ if (didPushChar) {
+ didPushChar = false;
+ return pushedChar;
+ }
+ return reader.read();
+ // Uncomment the following to do case insensitive parsing.
+ /*
+ if (retValue != -1) {
+ return (int)Character.toLowerCase((char)retValue);
+ }
+ return retValue;
+ */
+ }
+
+ /**
+ * Supports one character look ahead, this will throw if called twice
+ * in a row.
+ */
+ private void pushChar(int tempChar) {
+ if (didPushChar) {
+ // Should never happen.
+ throw new RuntimeException("Can not handle look ahead of more than one character");
+ }
+ didPushChar = true;
+ pushedChar = tempChar;
+ }
+}