blob: 00251a740ef69e631bea0c10ef0c7f0bbbad6ee3 [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 1999-2000 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25package javax.swing.text.html;
26
27import java.io.*;
28
29/**
30 * A CSS parser. This works by way of a delegate that implements the
31 * CSSParserCallback interface. The delegate is notified of the following
32 * events:
33 * <ul>
34 * <li>Import statement: <code>handleImport</code>
35 * <li>Selectors <code>handleSelector</code>. This is invoked for each
36 * string. For example if the Reader contained p, bar , a {}, the delegate
37 * would be notified 4 times, for 'p,' 'bar' ',' and 'a'.
38 * <li>When a rule starts, <code>startRule</code>
39 * <li>Properties in the rule via the <code>handleProperty</code>. This
40 * is invoked one per property/value key, eg font size: foo;, would
41 * cause the delegate to be notified once with a value of 'font size'.
42 * <li>Values in the rule via the <code>handleValue</code>, this is notified
43 * for the total value.
44 * <li>When a rule ends, <code>endRule</code>
45 * </ul>
46 * This will parse much more than CSS 1, and loosely implements the
47 * recommendation for <i>Forward-compatible parsing</i> in section
48 * 7.1 of the CSS spec found at:
49 * <a href=http://www.w3.org/TR/REC-CSS1>http://www.w3.org/TR/REC-CSS1</a>.
50 * If an error results in parsing, a RuntimeException will be thrown.
51 * <p>
52 * This will preserve case. If the callback wishes to treat certain poritions
53 * case insensitively (such as selectors), it should use toLowerCase, or
54 * something similar.
55 *
56 * @author Scott Violet
57 */
58class CSSParser {
59 // Parsing something like the following:
60 // (@rule | ruleset | block)*
61 //
62 // @rule (block | identifier)*; (block with {} ends @rule)
63 // block matching [] () {} (that is, [()] is a block, [(){}{[]}]
64 // is a block, ()[] is two blocks)
65 // identifier "*" | '*' | anything but a [](){} and whitespace
66 //
67 // ruleset selector decblock
68 // selector (identifier | (block, except block '{}') )*
69 // declblock declaration* block*
70 // declaration (identifier* stopping when identifier ends with :)
71 // (identifier* stopping when identifier ends with ;)
72 //
73 // comments /* */ can appear any where, and are stripped.
74
75
76 // identifier - letters, digits, dashes and escaped characters
77 // block starts with { ends with matching }, () [] and {} always occur
78 // in matching pairs, '' and "" also occur in pairs, except " may be
79
80
81 // Indicates the type of token being parsed.
82 private static final int IDENTIFIER = 1;
83 private static final int BRACKET_OPEN = 2;
84 private static final int BRACKET_CLOSE = 3;
85 private static final int BRACE_OPEN = 4;
86 private static final int BRACE_CLOSE = 5;
87 private static final int PAREN_OPEN = 6;
88 private static final int PAREN_CLOSE = 7;
89 private static final int END = -1;
90
91 private static final char[] charMapping = { 0, 0, '[', ']', '{', '}', '(',
92 ')', 0};
93
94
95 /** Set to true if one character has been read ahead. */
96 private boolean didPushChar;
97 /** The read ahead character. */
98 private int pushedChar;
99 /** Temporary place to hold identifiers. */
100 private StringBuffer unitBuffer;
101 /** Used to indicate blocks. */
102 private int[] unitStack;
103 /** Number of valid blocks. */
104 private int stackCount;
105 /** Holds the incoming CSS rules. */
106 private Reader reader;
107 /** Set to true when the first non @ rule is encountered. */
108 private boolean encounteredRuleSet;
109 /** Notified of state. */
110 private CSSParserCallback callback;
111 /** nextToken() inserts the string here. */
112 private char[] tokenBuffer;
113 /** Current number of chars in tokenBufferLength. */
114 private int tokenBufferLength;
115 /** Set to true if any whitespace is read. */
116 private boolean readWS;
117
118
119 // The delegate interface.
120 static interface CSSParserCallback {
121 /** Called when an @import is encountered. */
122 void handleImport(String importString);
123 // There is currently no way to distinguish between '"foo,"' and
124 // 'foo,'. But this generally isn't valid CSS. If it becomes
125 // a problem, handleSelector will have to be told if the string is
126 // quoted.
127 void handleSelector(String selector);
128 void startRule();
129 // Property names are mapped to lower case before being passed to
130 // the delegate.
131 void handleProperty(String property);
132 void handleValue(String value);
133 void endRule();
134 }
135
136 CSSParser() {
137 unitStack = new int[2];
138 tokenBuffer = new char[80];
139 unitBuffer = new StringBuffer();
140 }
141
142 void parse(Reader reader, CSSParserCallback callback,
143 boolean inRule) throws IOException {
144 this.callback = callback;
145 stackCount = tokenBufferLength = 0;
146 this.reader = reader;
147 encounteredRuleSet = false;
148 try {
149 if (inRule) {
150 parseDeclarationBlock();
151 }
152 else {
153 while (getNextStatement());
154 }
155 } finally {
156 callback = null;
157 reader = null;
158 }
159 }
160
161 /**
162 * Gets the next statement, returning false if the end is reached. A
163 * statement is either an @rule, or a ruleset.
164 */
165 private boolean getNextStatement() throws IOException {
166 unitBuffer.setLength(0);
167
168 int token = nextToken((char)0);
169
170 switch (token) {
171 case IDENTIFIER:
172 if (tokenBufferLength > 0) {
173 if (tokenBuffer[0] == '@') {
174 parseAtRule();
175 }
176 else {
177 encounteredRuleSet = true;
178 parseRuleSet();
179 }
180 }
181 return true;
182 case BRACKET_OPEN:
183 case BRACE_OPEN:
184 case PAREN_OPEN:
185 parseTillClosed(token);
186 return true;
187
188 case BRACKET_CLOSE:
189 case BRACE_CLOSE:
190 case PAREN_CLOSE:
191 // Shouldn't happen...
192 throw new RuntimeException("Unexpected top level block close");
193
194 case END:
195 return false;
196 }
197 return true;
198 }
199
200 /**
201 * Parses an @ rule, stopping at a matching brace pair, or ;.
202 */
203 private void parseAtRule() throws IOException {
204 // PENDING: make this more effecient.
205 boolean done = false;
206 boolean isImport = (tokenBufferLength == 7 &&
207 tokenBuffer[0] == '@' && tokenBuffer[1] == 'i' &&
208 tokenBuffer[2] == 'm' && tokenBuffer[3] == 'p' &&
209 tokenBuffer[4] == 'o' && tokenBuffer[5] == 'r' &&
210 tokenBuffer[6] == 't');
211
212 unitBuffer.setLength(0);
213 while (!done) {
214 int nextToken = nextToken(';');
215
216 switch (nextToken) {
217 case IDENTIFIER:
218 if (tokenBufferLength > 0 &&
219 tokenBuffer[tokenBufferLength - 1] == ';') {
220 --tokenBufferLength;
221 done = true;
222 }
223 if (tokenBufferLength > 0) {
224 if (unitBuffer.length() > 0 && readWS) {
225 unitBuffer.append(' ');
226 }
227 unitBuffer.append(tokenBuffer, 0, tokenBufferLength);
228 }
229 break;
230
231 case BRACE_OPEN:
232 if (unitBuffer.length() > 0 && readWS) {
233 unitBuffer.append(' ');
234 }
235 unitBuffer.append(charMapping[nextToken]);
236 parseTillClosed(nextToken);
237 done = true;
238 // Skip a tailing ';', not really to spec.
239 {
240 int nextChar = readWS();
241 if (nextChar != -1 && nextChar != ';') {
242 pushChar(nextChar);
243 }
244 }
245 break;
246
247 case BRACKET_OPEN: case PAREN_OPEN:
248 unitBuffer.append(charMapping[nextToken]);
249 parseTillClosed(nextToken);
250 break;
251
252 case BRACKET_CLOSE: case BRACE_CLOSE: case PAREN_CLOSE:
253 throw new RuntimeException("Unexpected close in @ rule");
254
255 case END:
256 done = true;
257 break;
258 }
259 }
260 if (isImport && !encounteredRuleSet) {
261 callback.handleImport(unitBuffer.toString());
262 }
263 }
264
265 /**
266 * Parses the next rule set, which is a selector followed by a
267 * declaration block.
268 */
269 private void parseRuleSet() throws IOException {
270 if (parseSelectors()) {
271 callback.startRule();
272 parseDeclarationBlock();
273 callback.endRule();
274 }
275 }
276
277 /**
278 * Parses a set of selectors, returning false if the end of the stream
279 * is reached.
280 */
281 private boolean parseSelectors() throws IOException {
282 // Parse the selectors
283 int nextToken;
284
285 if (tokenBufferLength > 0) {
286 callback.handleSelector(new String(tokenBuffer, 0,
287 tokenBufferLength));
288 }
289
290 unitBuffer.setLength(0);
291 for (;;) {
292 while ((nextToken = nextToken((char)0)) == IDENTIFIER) {
293 if (tokenBufferLength > 0) {
294 callback.handleSelector(new String(tokenBuffer, 0,
295 tokenBufferLength));
296 }
297 }
298 switch (nextToken) {
299 case BRACE_OPEN:
300 return true;
301
302 case BRACKET_OPEN: case PAREN_OPEN:
303 parseTillClosed(nextToken);
304 // Not too sure about this, how we handle this isn't very
305 // well spec'd.
306 unitBuffer.setLength(0);
307 break;
308
309 case BRACKET_CLOSE: case BRACE_CLOSE: case PAREN_CLOSE:
310 throw new RuntimeException("Unexpected block close in selector");
311
312 case END:
313 // Prematurely hit end.
314 return false;
315 }
316 }
317 }
318
319 /**
320 * Parses a declaration block. Which a number of declarations followed
321 * by a })].
322 */
323 private void parseDeclarationBlock() throws IOException {
324 for (;;) {
325 int token = parseDeclaration();
326 switch (token) {
327 case END: case BRACE_CLOSE:
328 return;
329
330 case BRACKET_CLOSE: case PAREN_CLOSE:
331 // Bail
332 throw new RuntimeException("Unexpected close in declaration block");
333 case IDENTIFIER:
334 break;
335 }
336 }
337 }
338
339 /**
340 * Parses a single declaration, which is an identifier a : and another
341 * identifier. This returns the last token seen.
342 */
343 // identifier+: identifier* ;|}
344 private int parseDeclaration() throws IOException {
345 int token;
346
347 if ((token = parseIdentifiers(':', false)) != IDENTIFIER) {
348 return token;
349 }
350 // Make the property name to lowercase
351 for (int counter = unitBuffer.length() - 1; counter >= 0; counter--) {
352 unitBuffer.setCharAt(counter, Character.toLowerCase
353 (unitBuffer.charAt(counter)));
354 }
355 callback.handleProperty(unitBuffer.toString());
356
357 token = parseIdentifiers(';', true);
358 callback.handleValue(unitBuffer.toString());
359 return token;
360 }
361
362 /**
363 * Parses identifiers until <code>extraChar</code> is encountered,
364 * returning the ending token, which will be IDENTIFIER if extraChar
365 * is found.
366 */
367 private int parseIdentifiers(char extraChar,
368 boolean wantsBlocks) throws IOException {
369 int nextToken;
370 int ubl;
371
372 unitBuffer.setLength(0);
373 for (;;) {
374 nextToken = nextToken(extraChar);
375
376 switch (nextToken) {
377 case IDENTIFIER:
378 if (tokenBufferLength > 0) {
379 if (tokenBuffer[tokenBufferLength - 1] == extraChar) {
380 if (--tokenBufferLength > 0) {
381 if (readWS && unitBuffer.length() > 0) {
382 unitBuffer.append(' ');
383 }
384 unitBuffer.append(tokenBuffer, 0,
385 tokenBufferLength);
386 }
387 return IDENTIFIER;
388 }
389 if (readWS && unitBuffer.length() > 0) {
390 unitBuffer.append(' ');
391 }
392 unitBuffer.append(tokenBuffer, 0, tokenBufferLength);
393 }
394 break;
395
396 case BRACKET_OPEN:
397 case BRACE_OPEN:
398 case PAREN_OPEN:
399 ubl = unitBuffer.length();
400 if (wantsBlocks) {
401 unitBuffer.append(charMapping[nextToken]);
402 }
403 parseTillClosed(nextToken);
404 if (!wantsBlocks) {
405 unitBuffer.setLength(ubl);
406 }
407 break;
408
409 case BRACE_CLOSE:
410 // No need to throw for these two, we return token and
411 // caller can do whatever.
412 case BRACKET_CLOSE:
413 case PAREN_CLOSE:
414 case END:
415 // Hit the end
416 return nextToken;
417 }
418 }
419 }
420
421 /**
422 * Parses till a matching block close is encountered. This is only
423 * appropriate to be called at the top level (no nesting).
424 */
425 private void parseTillClosed(int openToken) throws IOException {
426 int nextToken;
427 boolean done = false;
428
429 startBlock(openToken);
430 while (!done) {
431 nextToken = nextToken((char)0);
432 switch (nextToken) {
433 case IDENTIFIER:
434 if (unitBuffer.length() > 0 && readWS) {
435 unitBuffer.append(' ');
436 }
437 if (tokenBufferLength > 0) {
438 unitBuffer.append(tokenBuffer, 0, tokenBufferLength);
439 }
440 break;
441
442 case BRACKET_OPEN: case BRACE_OPEN: case PAREN_OPEN:
443 if (unitBuffer.length() > 0 && readWS) {
444 unitBuffer.append(' ');
445 }
446 unitBuffer.append(charMapping[nextToken]);
447 startBlock(nextToken);
448 break;
449
450 case BRACKET_CLOSE: case BRACE_CLOSE: case PAREN_CLOSE:
451 if (unitBuffer.length() > 0 && readWS) {
452 unitBuffer.append(' ');
453 }
454 unitBuffer.append(charMapping[nextToken]);
455 endBlock(nextToken);
456 if (!inBlock()) {
457 done = true;
458 }
459 break;
460
461 case END:
462 // Prematurely hit end.
463 throw new RuntimeException("Unclosed block");
464 }
465 }
466 }
467
468 /**
469 * Fetches the next token.
470 */
471 private int nextToken(char idChar) throws IOException {
472 readWS = false;
473
474 int nextChar = readWS();
475
476 switch (nextChar) {
477 case '\'':
478 readTill('\'');
479 if (tokenBufferLength > 0) {
480 tokenBufferLength--;
481 }
482 return IDENTIFIER;
483 case '"':
484 readTill('"');
485 if (tokenBufferLength > 0) {
486 tokenBufferLength--;
487 }
488 return IDENTIFIER;
489 case '[':
490 return BRACKET_OPEN;
491 case ']':
492 return BRACKET_CLOSE;
493 case '{':
494 return BRACE_OPEN;
495 case '}':
496 return BRACE_CLOSE;
497 case '(':
498 return PAREN_OPEN;
499 case ')':
500 return PAREN_CLOSE;
501 case -1:
502 return END;
503 default:
504 pushChar(nextChar);
505 getIdentifier(idChar);
506 return IDENTIFIER;
507 }
508 }
509
510 /**
511 * Gets an identifier, returning true if the length of the string is greater than 0,
512 * stopping when <code>stopChar</code>, whitespace, or one of {}()[] is
513 * hit.
514 */
515 // NOTE: this could be combined with readTill, as they contain somewhat
516 // similiar functionality.
517 private boolean getIdentifier(char stopChar) throws IOException {
518 boolean lastWasEscape = false;
519 boolean done = false;
520 int escapeCount = 0;
521 int escapeChar = 0;
522 int nextChar;
523 int intStopChar = (int)stopChar;
524 // 1 for '\', 2 for valid escape char [0-9a-fA-F], 3 for
525 // stop character (white space, ()[]{}) 0 otherwise
526 short type;
527 int escapeOffset = 0;
528
529 tokenBufferLength = 0;
530 while (!done) {
531 nextChar = readChar();
532 switch (nextChar) {
533 case '\\':
534 type = 1;
535 break;
536
537 case '0': case '1': case '2': case '3': case '4': case '5':
538 case '6': case '7': case '8': case '9':
539 type = 2;
540 escapeOffset = nextChar - '0';
541 break;
542
543 case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
544 type = 2;
545 escapeOffset = nextChar - 'a' + 10;
546 break;
547
548 case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
549 type = 2;
550 escapeOffset = nextChar - 'A' + 10;
551 break;
552
553 case '\'': case '"': case '[': case ']': case '{': case '}':
554 case '(': case ')':
555 case ' ': case '\n': case '\t': case '\r':
556 type = 3;
557 break;
558
559 case '/':
560 type = 4;
561 break;
562
563 case -1:
564 // Reached the end
565 done = true;
566 type = 0;
567 break;
568
569 default:
570 type = 0;
571 break;
572 }
573 if (lastWasEscape) {
574 if (type == 2) {
575 // Continue with escape.
576 escapeChar = escapeChar * 16 + escapeOffset;
577 if (++escapeCount == 4) {
578 lastWasEscape = false;
579 append((char)escapeChar);
580 }
581 }
582 else {
583 // no longer escaped
584 lastWasEscape = false;
585 if (escapeCount > 0) {
586 append((char)escapeChar);
587 // Make this simpler, reprocess the character.
588 pushChar(nextChar);
589 }
590 else if (!done) {
591 append((char)nextChar);
592 }
593 }
594 }
595 else if (!done) {
596 if (type == 1) {
597 lastWasEscape = true;
598 escapeChar = escapeCount = 0;
599 }
600 else if (type == 3) {
601 done = true;
602 pushChar(nextChar);
603 }
604 else if (type == 4) {
605 // Potential comment
606 nextChar = readChar();
607 if (nextChar == '*') {
608 done = true;
609 readComment();
610 readWS = true;
611 }
612 else {
613 append('/');
614 if (nextChar == -1) {
615 done = true;
616 }
617 else {
618 pushChar(nextChar);
619 }
620 }
621 }
622 else {
623 append((char)nextChar);
624 if (nextChar == intStopChar) {
625 done = true;
626 }
627 }
628 }
629 }
630 return (tokenBufferLength > 0);
631 }
632
633 /**
634 * Reads till a <code>stopChar</code> is encountered, escaping characters
635 * as necessary.
636 */
637 private void readTill(char stopChar) throws IOException {
638 boolean lastWasEscape = false;
639 int escapeCount = 0;
640 int escapeChar = 0;
641 int nextChar;
642 boolean done = false;
643 int intStopChar = (int)stopChar;
644 // 1 for '\', 2 for valid escape char [0-9a-fA-F], 0 otherwise
645 short type;
646 int escapeOffset = 0;
647
648 tokenBufferLength = 0;
649 while (!done) {
650 nextChar = readChar();
651 switch (nextChar) {
652 case '\\':
653 type = 1;
654 break;
655
656 case '0': case '1': case '2': case '3': case '4':case '5':
657 case '6': case '7': case '8': case '9':
658 type = 2;
659 escapeOffset = nextChar - '0';
660 break;
661
662 case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
663 type = 2;
664 escapeOffset = nextChar - 'a' + 10;
665 break;
666
667 case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
668 type = 2;
669 escapeOffset = nextChar - 'A' + 10;
670 break;
671
672 case -1:
673 // Prematurely reached the end!
674 throw new RuntimeException("Unclosed " + stopChar);
675
676 default:
677 type = 0;
678 break;
679 }
680 if (lastWasEscape) {
681 if (type == 2) {
682 // Continue with escape.
683 escapeChar = escapeChar * 16 + escapeOffset;
684 if (++escapeCount == 4) {
685 lastWasEscape = false;
686 append((char)escapeChar);
687 }
688 }
689 else {
690 // no longer escaped
691 if (escapeCount > 0) {
692 append((char)escapeChar);
693 if (type == 1) {
694 lastWasEscape = true;
695 escapeChar = escapeCount = 0;
696 }
697 else {
698 if (nextChar == intStopChar) {
699 done = true;
700 }
701 append((char)nextChar);
702 lastWasEscape = false;
703 }
704 }
705 else {
706 append((char)nextChar);
707 lastWasEscape = false;
708 }
709 }
710 }
711 else if (type == 1) {
712 lastWasEscape = true;
713 escapeChar = escapeCount = 0;
714 }
715 else {
716 if (nextChar == intStopChar) {
717 done = true;
718 }
719 append((char)nextChar);
720 }
721 }
722 }
723
724 private void append(char character) {
725 if (tokenBufferLength == tokenBuffer.length) {
726 char[] newBuffer = new char[tokenBuffer.length * 2];
727 System.arraycopy(tokenBuffer, 0, newBuffer, 0, tokenBuffer.length);
728 tokenBuffer = newBuffer;
729 }
730 tokenBuffer[tokenBufferLength++] = character;
731 }
732
733 /**
734 * Parses a comment block.
735 */
736 private void readComment() throws IOException {
737 int nextChar;
738
739 for(;;) {
740 nextChar = readChar();
741 switch (nextChar) {
742 case -1:
743 throw new RuntimeException("Unclosed comment");
744 case '*':
745 nextChar = readChar();
746 if (nextChar == '/') {
747 return;
748 }
749 else if (nextChar == -1) {
750 throw new RuntimeException("Unclosed comment");
751 }
752 else {
753 pushChar(nextChar);
754 }
755 break;
756 default:
757 break;
758 }
759 }
760 }
761
762 /**
763 * Called when a block start is encountered ({[.
764 */
765 private void startBlock(int startToken) {
766 if (stackCount == unitStack.length) {
767 int[] newUS = new int[stackCount * 2];
768
769 System.arraycopy(unitStack, 0, newUS, 0, stackCount);
770 unitStack = newUS;
771 }
772 unitStack[stackCount++] = startToken;
773 }
774
775 /**
776 * Called when an end block is encountered )]}
777 */
778 private void endBlock(int endToken) {
779 int startToken;
780
781 switch (endToken) {
782 case BRACKET_CLOSE:
783 startToken = BRACKET_OPEN;
784 break;
785 case BRACE_CLOSE:
786 startToken = BRACE_OPEN;
787 break;
788 case PAREN_CLOSE:
789 startToken = PAREN_OPEN;
790 break;
791 default:
792 // Will never happen.
793 startToken = -1;
794 break;
795 }
796 if (stackCount > 0 && unitStack[stackCount - 1] == startToken) {
797 stackCount--;
798 }
799 else {
800 // Invalid state, should do something.
801 throw new RuntimeException("Unmatched block");
802 }
803 }
804
805 /**
806 * @return true if currently in a block.
807 */
808 private boolean inBlock() {
809 return (stackCount > 0);
810 }
811
812 /**
813 * Skips any white space, returning the character after the white space.
814 */
815 private int readWS() throws IOException {
816 int nextChar;
817 while ((nextChar = readChar()) != -1 &&
818 Character.isWhitespace((char)nextChar)) {
819 readWS = true;
820 }
821 return nextChar;
822 }
823
824 /**
825 * Reads a character from the stream.
826 */
827 private int readChar() throws IOException {
828 if (didPushChar) {
829 didPushChar = false;
830 return pushedChar;
831 }
832 return reader.read();
833 // Uncomment the following to do case insensitive parsing.
834 /*
835 if (retValue != -1) {
836 return (int)Character.toLowerCase((char)retValue);
837 }
838 return retValue;
839 */
840 }
841
842 /**
843 * Supports one character look ahead, this will throw if called twice
844 * in a row.
845 */
846 private void pushChar(int tempChar) {
847 if (didPushChar) {
848 // Should never happen.
849 throw new RuntimeException("Can not handle look ahead of more than one character");
850 }
851 didPushChar = true;
852 pushedChar = tempChar;
853 }
854}