FileDocCategorySizeDatePackage
CSSParser.javaAPI DocJava SE 5 API20967Fri Aug 26 14:58:18 BST 2005javax.swing.text.html

CSSParser.java

/*
 * @(#)CSSParser.java	1.8 03/12/19
 *
 * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
 * SUN 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
 * @version 1.8 12/19/03
 */
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;
    }
}