001: /*
002: * Copyright 2000,2005 wingS development team.
003: *
004: * This file is part of wingS (http://wingsframework.org).
005: *
006: * wingS is free software; you can redistribute it and/or modify
007: * it under the terms of the GNU Lesser General Public License
008: * as published by the Free Software Foundation; either version 2.1
009: * of the License, or (at your option) any later version.
010: *
011: * Please see COPYING for the complete licence.
012: */
013: package org.wings.style;
014:
015: import java.io.IOException;
016: import java.io.Reader;
017:
018: import org.wings.util.SStringBuilder;
019:
020: /**
021: * A CSS parser. This works by way of a delegate that implements the
022: * CSSParserCallback interface. The delegate is notified of the following
023: * events:
024: * <ul>
025: * <li>Import statement: <code>handleImport</code>
026: * <li>Selectors <code>handleSelector</code>. This is invoked for each
027: * string. For example if the Reader contained p, bar , a {}, the delegate
028: * would be notified 4 times, for 'p,' 'bar' ',' and 'a'.
029: * <li>When a rule starts, <code>startRule</code>
030: * <li>Properties in the rule via the <code>handleProperty</code>. This
031: * is invoked one per property/value key, eg font size: foo;, would
032: * cause the delegate to be notified once with a value of 'font size'.
033: * <li>Values in the rule via the <code>handleValue</code>, this is notified
034: * for the total value.
035: * <li>When a rule ends, <code>endRule</code>
036: * </ul>
037: * This will parse much more than CSS 1, and loosely implements the
038: * recommendation for <i>Forward-compatible parsing</i> in section
039: * 7.1 of the CSS spec found at:
040: * <a href=http://www.w3.org/TR/REC-CSS1>http://www.w3.org/TR/REC-CSS1</a>.
041: * If an error results in parsing, a RuntimeException will be thrown.
042: * <p/>
043: * This will preserve case. If the callback wishes to treat certain poritions
044: * case insensitively (such as selectors), it should use toLowerCase, or
045: * something similar.
046: *
047: * @author Scott Violet
048: * @version 1.5 03/20/00
049: */
050: class CSSParser {
051: // Parsing something like the following:
052: // (@rule | ruleset | block)*
053: //
054: // @rule (block | identifier)*; (block with {} ends @rule)
055: // block matching [] () {} (that is, [()] is a block, [(){}{[]}]
056: // is a block, ()[] is two blocks)
057: // identifier "*" | '*' | anything but a [](){} and whitespace
058: //
059: // ruleset selector decblock
060: // selector (identifier | (block, except block '{}') )*
061: // declblock declaration* block*
062: // declaration (identifier* stopping when identifier ends with :)
063: // (identifier* stopping when identifier ends with ;)
064: //
065: // comments /* */ can appear any where, and are stripped.
066:
067: // identifier - letters, digits, dashes and escaped characters
068: // block starts with { ends with matching }, () [] and {} always occur
069: // in matching pairs, '' and "" also occur in pairs, except " may be
070:
071: // Indicates the type of token being parsed.
072: private static final int IDENTIFIER = 1;
073: private static final int BRACKET_OPEN = 2;
074: private static final int BRACKET_CLOSE = 3;
075: private static final int BRACE_OPEN = 4;
076: private static final int BRACE_CLOSE = 5;
077: private static final int PAREN_OPEN = 6;
078: private static final int PAREN_CLOSE = 7;
079: private static final int END = -1;
080:
081: private static final char[] charMapping = { 0, 0, '[', ']', '{',
082: '}', '(', ')', 0 };
083:
084: /**
085: * Set to true if one character has been read ahead.
086: */
087: private boolean didPushChar;
088: /**
089: * The read ahead character.
090: */
091: private int pushedChar;
092: /**
093: * Temporary place to hold identifiers.
094: */
095: private SStringBuilder unitBuffer;
096: /**
097: * Used to indicate blocks.
098: */
099: private int[] unitStack;
100: /**
101: * Number of valid blocks.
102: */
103: private int stackCount;
104: /**
105: * Holds the incoming CSS rules.
106: */
107: private Reader reader;
108: /**
109: * Set to true when the first non @ rule is encountered.
110: */
111: private boolean encounteredRuleSet;
112: /**
113: * Notified of state.
114: */
115: private CSSParserCallback callback;
116: /**
117: * nextToken() inserts the string here.
118: */
119: private char[] tokenBuffer;
120: /**
121: * Current number of chars in tokenBufferLength.
122: */
123: private int tokenBufferLength;
124: /**
125: * Set to true if any whitespace is read.
126: */
127: private boolean readWS;
128:
129: // The delegate interface.
130: static interface CSSParserCallback {
131: /**
132: * Called when an @import is encountered.
133: */
134: void handleImport(String importString);
135:
136: // There is currently no way to distinguish between '"foo,"' and
137: // 'foo,'. But this generally isn't valid CSS. If it becomes
138: // a problem, handleSelector will have to be told if the string is
139: // quoted.
140: void handleSelector(String selector);
141:
142: void startRule();
143:
144: // Property names are mapped to lower case before being passed to
145: // the delegate.
146: void handleProperty(String property);
147:
148: void handleValue(String value);
149:
150: void endRule();
151: }
152:
153: CSSParser() {
154: unitStack = new int[2];
155: tokenBuffer = new char[80];
156: unitBuffer = new SStringBuilder();
157: }
158:
159: void parse(Reader reader, CSSParserCallback callback, boolean inRule)
160: throws IOException {
161: this .callback = callback;
162: stackCount = tokenBufferLength = 0;
163: this .reader = reader;
164: encounteredRuleSet = false;
165: try {
166: if (inRule) {
167: parseDeclarationBlock();
168: } else {
169: while (getNextStatement())
170: ;
171: }
172: } finally {
173: callback = null;
174: reader = null;
175: }
176: }
177:
178: /**
179: * Gets the next statement, returning false if the end is reached. A
180: * statement is either an @rule, or a ruleset.
181: */
182: private boolean getNextStatement() throws IOException {
183: unitBuffer.setLength(0);
184:
185: int token = nextToken((char) 0);
186:
187: switch (token) {
188: case IDENTIFIER:
189: if (tokenBufferLength > 0) {
190: if (tokenBuffer[0] == '@') {
191: parseAtRule();
192: } else {
193: encounteredRuleSet = true;
194: parseRuleSet();
195: }
196: }
197: return true;
198: case BRACKET_OPEN:
199: case BRACE_OPEN:
200: case PAREN_OPEN:
201: parseTillClosed(token);
202: return true;
203:
204: case BRACKET_CLOSE:
205: case BRACE_CLOSE:
206: case PAREN_CLOSE:
207: // Shouldn't happen...
208: throw new RuntimeException(
209: "Unexpected top level block close");
210:
211: case END:
212: return false;
213: }
214: return true;
215: }
216:
217: /**
218: * Parses an @ rule, stopping at a matching brace pair, or ;.
219: */
220: private void parseAtRule() throws IOException {
221: // PENDING: make this more effecient.
222: boolean done = false;
223: boolean isImport = (tokenBufferLength == 7
224: && tokenBuffer[0] == '@' && tokenBuffer[1] == 'i'
225: && tokenBuffer[2] == 'm' && tokenBuffer[3] == 'p'
226: && tokenBuffer[4] == 'o' && tokenBuffer[5] == 'r' && tokenBuffer[6] == 't');
227:
228: unitBuffer.setLength(0);
229: while (!done) {
230: int nextToken = nextToken(';');
231:
232: switch (nextToken) {
233: case IDENTIFIER:
234: if (tokenBufferLength > 0
235: && tokenBuffer[tokenBufferLength - 1] == ';') {
236: --tokenBufferLength;
237: done = true;
238: }
239: if (tokenBufferLength > 0) {
240: if (unitBuffer.length() > 0 && readWS) {
241: unitBuffer.append(' ');
242: }
243: unitBuffer
244: .append(tokenBuffer, 0, tokenBufferLength);
245: }
246: break;
247:
248: case BRACE_OPEN:
249: if (unitBuffer.length() > 0 && readWS) {
250: unitBuffer.append(' ');
251: }
252: unitBuffer.append(charMapping[nextToken]);
253: parseTillClosed(nextToken);
254: done = true;
255: // Skip a tailing ';', not really to spec.
256: {
257: int nextChar = readWS();
258: if (nextChar != -1 && nextChar != ';') {
259: pushChar(nextChar);
260: }
261: }
262: break;
263:
264: case BRACKET_OPEN:
265: case PAREN_OPEN:
266: unitBuffer.append(charMapping[nextToken]);
267: parseTillClosed(nextToken);
268: break;
269:
270: case BRACKET_CLOSE:
271: case BRACE_CLOSE:
272: case PAREN_CLOSE:
273: throw new RuntimeException("Unexpected close in @ rule");
274:
275: case END:
276: done = true;
277: break;
278: }
279: }
280: if (isImport && !encounteredRuleSet) {
281: callback.handleImport(unitBuffer.toString());
282: }
283: }
284:
285: /**
286: * Parses the next rule set, which is a selector followed by a
287: * declaration block.
288: */
289: private void parseRuleSet() throws IOException {
290: if (parseSelectors()) {
291: callback.startRule();
292: parseDeclarationBlock();
293: callback.endRule();
294: }
295: }
296:
297: /**
298: * Parses a set of selectors, returning false if the end of the stream
299: * is reached.
300: */
301: private boolean parseSelectors() throws IOException {
302: // Parse the selectors
303: int nextToken;
304:
305: if (tokenBufferLength > 0) {
306: callback.handleSelector(new String(tokenBuffer, 0,
307: tokenBufferLength));
308: }
309:
310: unitBuffer.setLength(0);
311: for (;;) {
312: while ((nextToken = nextToken((char) 0)) == IDENTIFIER) {
313: if (tokenBufferLength > 0) {
314: callback.handleSelector(new String(tokenBuffer, 0,
315: tokenBufferLength));
316: }
317: }
318: switch (nextToken) {
319: case BRACE_OPEN:
320: return true;
321:
322: case BRACKET_OPEN:
323: case PAREN_OPEN:
324: parseTillClosed(nextToken);
325: // Not too sure about this, how we handle this isn't very
326: // well spec'd.
327: unitBuffer.setLength(0);
328: break;
329:
330: case BRACKET_CLOSE:
331: case BRACE_CLOSE:
332: case PAREN_CLOSE:
333: throw new RuntimeException(
334: "Unexpected block close in selector");
335:
336: case END:
337: // Prematurely hit end.
338: return false;
339: }
340: }
341: }
342:
343: /**
344: * Parses a declaration block. Which a number of declarations followed
345: * by a })].
346: */
347: private void parseDeclarationBlock() throws IOException {
348: for (;;) {
349: int token = parseDeclaration();
350: switch (token) {
351: case END:
352: case BRACE_CLOSE:
353: return;
354:
355: case BRACKET_CLOSE:
356: case PAREN_CLOSE:
357: // Bail
358: throw new RuntimeException(
359: "Unexpected close in declaration block");
360: case IDENTIFIER:
361: break;
362: }
363: }
364: }
365:
366: /**
367: * Parses a single declaration, which is an identifier a : and another
368: * identifier. This returns the last token seen.
369: */
370: // identifier+: identifier* ;|}
371: private int parseDeclaration() throws IOException {
372: int token;
373:
374: if ((token = parseIdentifiers(':', false)) != IDENTIFIER) {
375: return token;
376: }
377: // Make the property name to lowercase
378: for (int counter = unitBuffer.length() - 1; counter >= 0; counter--) {
379: unitBuffer.setCharAt(counter, Character
380: .toLowerCase(unitBuffer.charAt(counter)));
381: }
382: callback.handleProperty(unitBuffer.toString());
383:
384: token = parseIdentifiers(';', true);
385: callback.handleValue(unitBuffer.toString());
386: return token;
387: }
388:
389: /**
390: * Parses identifiers until <code>extraChar</code> is encountered,
391: * returning the ending token, which will be IDENTIFIER if extraChar
392: * is found.
393: */
394: private int parseIdentifiers(char extraChar, boolean wantsBlocks)
395: throws IOException {
396: int nextToken;
397: int ubl;
398:
399: unitBuffer.setLength(0);
400: for (;;) {
401: nextToken = nextToken(extraChar);
402:
403: switch (nextToken) {
404: case IDENTIFIER:
405: if (tokenBufferLength > 0) {
406: if (tokenBuffer[tokenBufferLength - 1] == extraChar) {
407: if (--tokenBufferLength > 0) {
408: if (readWS && unitBuffer.length() > 0) {
409: unitBuffer.append(' ');
410: }
411: unitBuffer.append(tokenBuffer, 0,
412: tokenBufferLength);
413: }
414: return IDENTIFIER;
415: }
416: if (readWS && unitBuffer.length() > 0) {
417: unitBuffer.append(' ');
418: }
419: unitBuffer
420: .append(tokenBuffer, 0, tokenBufferLength);
421: }
422: break;
423:
424: case BRACKET_OPEN:
425: case BRACE_OPEN:
426: case PAREN_OPEN:
427: ubl = unitBuffer.length();
428: if (wantsBlocks) {
429: unitBuffer.append(charMapping[nextToken]);
430: }
431: parseTillClosed(nextToken);
432: if (!wantsBlocks) {
433: unitBuffer.setLength(ubl);
434: }
435: break;
436:
437: case BRACE_CLOSE:
438: // No need to throw for these two, we return token and
439: // caller can do whatever.
440: case BRACKET_CLOSE:
441: case PAREN_CLOSE:
442: case END:
443: // Hit the end
444: return nextToken;
445: }
446: }
447: }
448:
449: /**
450: * Parses till a matching block close is encountered. This is only
451: * appropriate to be called at the top level (no nesting).
452: */
453: private void parseTillClosed(int openToken) throws IOException {
454: int nextToken;
455: boolean done = false;
456:
457: startBlock(openToken);
458: while (!done) {
459: nextToken = nextToken((char) 0);
460: switch (nextToken) {
461: case IDENTIFIER:
462: if (unitBuffer.length() > 0 && readWS) {
463: unitBuffer.append(' ');
464: }
465: if (tokenBufferLength > 0) {
466: unitBuffer
467: .append(tokenBuffer, 0, tokenBufferLength);
468: }
469: break;
470:
471: case BRACKET_OPEN:
472: case BRACE_OPEN:
473: case PAREN_OPEN:
474: if (unitBuffer.length() > 0 && readWS) {
475: unitBuffer.append(' ');
476: }
477: unitBuffer.append(charMapping[nextToken]);
478: startBlock(nextToken);
479: break;
480:
481: case BRACKET_CLOSE:
482: case BRACE_CLOSE:
483: case PAREN_CLOSE:
484: if (unitBuffer.length() > 0 && readWS) {
485: unitBuffer.append(' ');
486: }
487: unitBuffer.append(charMapping[nextToken]);
488: endBlock(nextToken);
489: if (!inBlock()) {
490: done = true;
491: }
492: break;
493:
494: case END:
495: // Prematurely hit end.
496: throw new RuntimeException("Unclosed block");
497: }
498: }
499: }
500:
501: /**
502: * Fetches the next token.
503: */
504: private int nextToken(char idChar) throws IOException {
505: readWS = false;
506:
507: int nextChar = readWS();
508:
509: switch (nextChar) {
510: case '\'':
511: readTill('\'');
512: if (tokenBufferLength > 0) {
513: tokenBufferLength--;
514: }
515: return IDENTIFIER;
516: case '"':
517: readTill('"');
518: if (tokenBufferLength > 0) {
519: tokenBufferLength--;
520: }
521: return IDENTIFIER;
522: case '[':
523: return BRACKET_OPEN;
524: case ']':
525: return BRACKET_CLOSE;
526: case '{':
527: return BRACE_OPEN;
528: case '}':
529: return BRACE_CLOSE;
530: case '(':
531: return PAREN_OPEN;
532: case ')':
533: return PAREN_CLOSE;
534: case -1:
535: return END;
536: default:
537: pushChar(nextChar);
538: getIdentifier(idChar);
539: return IDENTIFIER;
540: }
541: }
542:
543: /**
544: * Gets an identifier, returning true if the length of the string is greater than 0,
545: * stopping when <code>stopChar</code>, whitespace, or one of {}()[] is
546: * hit.
547: */
548: // NOTE: this could be combined with readTill, as they contain somewhat
549: // similiar functionality.
550: private boolean getIdentifier(char stopChar) throws IOException {
551: boolean lastWasEscape = false;
552: boolean done = false;
553: int escapeCount = 0;
554: int escapeChar = 0;
555: int nextChar;
556: int intStopChar = (int) stopChar;
557: // 1 for '\', 2 for valid escape char [0-9a-fA-F], 3 for
558: // stop character (white space, ()[]{}) 0 otherwise
559: short type;
560: int escapeOffset = 0;
561:
562: tokenBufferLength = 0;
563: while (!done) {
564: nextChar = readChar();
565: switch (nextChar) {
566: case '\\':
567: type = 1;
568: break;
569:
570: case '0':
571: case '1':
572: case '2':
573: case '3':
574: case '4':
575: case '5':
576: case '6':
577: case '7':
578: case '8':
579: case '9':
580: type = 2;
581: escapeOffset = nextChar - '0';
582: break;
583:
584: case 'a':
585: case 'b':
586: case 'c':
587: case 'd':
588: case 'e':
589: case 'f':
590: type = 2;
591: escapeOffset = nextChar - 'a' + 10;
592: break;
593:
594: case 'A':
595: case 'B':
596: case 'C':
597: case 'D':
598: case 'E':
599: case 'F':
600: type = 2;
601: escapeOffset = nextChar - 'A' + 10;
602: break;
603:
604: case '\'':
605: case '"':
606: case '[':
607: case ']':
608: case '{':
609: case '}':
610: case '(':
611: case ')':
612: case ' ':
613: case '\n':
614: case '\t':
615: case '\r':
616: type = 3;
617: break;
618:
619: case '/':
620: type = 4;
621: break;
622:
623: case -1:
624: // Reached the end
625: done = true;
626: type = 0;
627: break;
628:
629: default:
630: type = 0;
631: break;
632: }
633: if (lastWasEscape) {
634: if (type == 2) {
635: // Continue with escape.
636: escapeChar = escapeChar * 16 + escapeOffset;
637: if (++escapeCount == 4) {
638: lastWasEscape = false;
639: append((char) escapeChar);
640: }
641: } else {
642: // no longer escaped
643: lastWasEscape = false;
644: if (escapeCount > 0) {
645: append((char) escapeChar);
646: // Make this simpler, reprocess the character.
647: pushChar(nextChar);
648: } else if (!done) {
649: append((char) nextChar);
650: }
651: }
652: } else if (!done) {
653: if (type == 1) {
654: lastWasEscape = true;
655: escapeChar = escapeCount = 0;
656: } else if (type == 3) {
657: done = true;
658: pushChar(nextChar);
659: } else if (type == 4) {
660: // Potential comment
661: nextChar = readChar();
662: if (nextChar == '*') {
663: done = true;
664: readComment();
665: readWS = true;
666: } else {
667: append('/');
668: if (nextChar == -1) {
669: done = true;
670: } else {
671: pushChar(nextChar);
672: }
673: }
674: } else {
675: append((char) nextChar);
676: if (nextChar == intStopChar) {
677: done = true;
678: }
679: }
680: }
681: }
682: return (tokenBufferLength > 0);
683: }
684:
685: /**
686: * Reads till a <code>stopChar</code> is encountered, escaping characters
687: * as necessary.
688: */
689: private void readTill(char stopChar) throws IOException {
690: boolean lastWasEscape = false;
691: int escapeCount = 0;
692: int escapeChar = 0;
693: int nextChar;
694: boolean done = false;
695: int intStopChar = (int) stopChar;
696: // 1 for '\', 2 for valid escape char [0-9a-fA-F], 0 otherwise
697: short type;
698: int escapeOffset = 0;
699:
700: tokenBufferLength = 0;
701: while (!done) {
702: nextChar = readChar();
703: switch (nextChar) {
704: case '\\':
705: type = 1;
706: break;
707:
708: case '0':
709: case '1':
710: case '2':
711: case '3':
712: case '4':
713: case '5':
714: case '6':
715: case '7':
716: case '8':
717: case '9':
718: type = 2;
719: escapeOffset = nextChar - '0';
720: break;
721:
722: case 'a':
723: case 'b':
724: case 'c':
725: case 'd':
726: case 'e':
727: case 'f':
728: type = 2;
729: escapeOffset = nextChar - 'a' + 10;
730: break;
731:
732: case 'A':
733: case 'B':
734: case 'C':
735: case 'D':
736: case 'E':
737: case 'F':
738: type = 2;
739: escapeOffset = nextChar - 'A' + 10;
740: break;
741:
742: case -1:
743: // Prematurely reached the end!
744: throw new RuntimeException("Unclosed " + stopChar);
745:
746: default:
747: type = 0;
748: break;
749: }
750: if (lastWasEscape) {
751: if (type == 2) {
752: // Continue with escape.
753: escapeChar = escapeChar * 16 + escapeOffset;
754: if (++escapeCount == 4) {
755: lastWasEscape = false;
756: append((char) escapeChar);
757: }
758: } else {
759: // no longer escaped
760: if (escapeCount > 0) {
761: append((char) escapeChar);
762: if (type == 1) {
763: lastWasEscape = true;
764: escapeChar = escapeCount = 0;
765: } else {
766: if (nextChar == intStopChar) {
767: done = true;
768: }
769: append((char) nextChar);
770: lastWasEscape = false;
771: }
772: } else {
773: append((char) nextChar);
774: lastWasEscape = false;
775: }
776: }
777: } else if (type == 1) {
778: lastWasEscape = true;
779: escapeChar = escapeCount = 0;
780: } else {
781: if (nextChar == intStopChar) {
782: done = true;
783: }
784: append((char) nextChar);
785: }
786: }
787: }
788:
789: private void append(char character) {
790: if (tokenBufferLength == tokenBuffer.length) {
791: char[] newBuffer = new char[tokenBuffer.length * 2];
792: System.arraycopy(tokenBuffer, 0, newBuffer, 0,
793: tokenBuffer.length);
794: tokenBuffer = newBuffer;
795: }
796: tokenBuffer[tokenBufferLength++] = character;
797: }
798:
799: /**
800: * Parses a comment block.
801: */
802: private void readComment() throws IOException {
803: int nextChar;
804:
805: for (;;) {
806: nextChar = readChar();
807: switch (nextChar) {
808: case -1:
809: throw new RuntimeException("Unclosed comment");
810: case '*':
811: nextChar = readChar();
812: if (nextChar == '/') {
813: return;
814: } else if (nextChar == -1) {
815: throw new RuntimeException("Unclosed comment");
816: } else {
817: pushChar(nextChar);
818: }
819: break;
820: default:
821: break;
822: }
823: }
824: }
825:
826: /**
827: * Called when a block start is encountered ({[.
828: */
829: private void startBlock(int startToken) {
830: if (stackCount == unitStack.length) {
831: int[] newUS = new int[stackCount * 2];
832:
833: System.arraycopy(unitStack, 0, newUS, 0, stackCount);
834: unitStack = newUS;
835: }
836: unitStack[stackCount++] = startToken;
837: }
838:
839: /**
840: * Called when an end block is encountered )]}
841: */
842: private void endBlock(int endToken) {
843: int startToken;
844:
845: switch (endToken) {
846: case BRACKET_CLOSE:
847: startToken = BRACKET_OPEN;
848: break;
849: case BRACE_CLOSE:
850: startToken = BRACE_OPEN;
851: break;
852: case PAREN_CLOSE:
853: startToken = PAREN_OPEN;
854: break;
855: default:
856: // Will never happen.
857: startToken = -1;
858: break;
859: }
860: if (stackCount > 0 && unitStack[stackCount - 1] == startToken) {
861: stackCount--;
862: } else {
863: // Invalid state, should do something.
864: throw new RuntimeException("Unmatched block");
865: }
866: }
867:
868: /**
869: * @return true if currently in a block.
870: */
871: private boolean inBlock() {
872: return (stackCount > 0);
873: }
874:
875: /**
876: * Skips any white space, returning the character after the white space.
877: */
878: private int readWS() throws IOException {
879: int nextChar;
880: while ((nextChar = readChar()) != -1
881: && Character.isWhitespace((char) nextChar)) {
882: readWS = true;
883: }
884: return nextChar;
885: }
886:
887: /**
888: * Reads a character from the stream.
889: */
890: private int readChar() throws IOException {
891: if (didPushChar) {
892: didPushChar = false;
893: return pushedChar;
894: }
895: return reader.read();
896: // Uncomment the following to do case insensitive parsing.
897: /*
898: if (retValue != -1) {
899: return (int)Character.toLowerCase((char)retValue);
900: }
901: return retValue;
902: */
903: }
904:
905: /**
906: * Supports one character look ahead, this will throw if called twice
907: * in a row.
908: */
909: private void pushChar(int tempChar) {
910: if (didPushChar) {
911: // Should never happen.
912: throw new RuntimeException(
913: "Can not handle look ahead of more than one character");
914: }
915: didPushChar = true;
916: pushedChar = tempChar;
917: }
918: }
|