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  */
  25 package javax.swing.text.html;
  26 
  27 import 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  */
  58 class 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 }