You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
855 lines
26 KiB
855 lines
26 KiB
/*
|
|
* Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved.
|
|
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*/
|
|
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
|
|
// similar 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;
|
|
}
|
|
}
|