1/* 2 * Copyright (c) 1999, 2013, Oracle and/or its affiliates. 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. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * 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 // similar 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} 855