View Javadoc
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 }