add IndentingReader
[sbp.git] / src / edu / berkeley / sbp / misc / IndentingReader.java
1 // Copyright 2007 all rights reserved; see LICENSE file for BSD-style license
2
3 package edu.berkeley.sbp.misc;
4
5 import java.util.*;
6 import java.io.*;
7
8 /**
9  *  This Reader inserts special characters into the stream to indicate
10  *  when the indentation level has increased or decreased.<p>
11  *  
12  *  Lines are separated by LF characters (ASCII/Unicode 0x0A).  The
13  *  indentation of a line is defined to be the number of contiguous
14  *  spaces (ASCII/Unicode 0x20).  A blank line is a line consisting
15  *  only of zero or more spaces.  <p>
16  *
17  *  If the indentation of the current line is <i>greater than</i> the
18  *  indentation of the most recent non-blank line, the last space
19  *  character of the current line's indentation will be <i>replaced
20  *  by</i> an <tt>updent</tt> character. (FIXME not quite right)<p>
21  *
22  *  If the indentation of the next non-blank line is <i>less than</i>
23  *  the indentation of the current line, one or more <tt>downdent</tt>
24  *  characters will be <i>inserted</i> immediately after the last
25  *  non-whitespace character on the current line. <p>
26  *
27  *  These rules have two goals:
28  *
29  *  <ul> <li> Whitespace never appears immediately after an
30  *            <tt>updent</tt> or immediately before a
31  *            <tt>downdent</tt>.  This simplifies grammars which are
32  *            based on these characters.
33  *
34  *       <li> Blank lines have no effect on the placement of
35  *            <tt>updent</tt> and <tt>downdent</tt>.
36  *
37  *       <li> Every non-space character from the original stream
38  *            appears in the modified stream.  Furthermore, these
39  *            characters appear at exactly the same row and column as
40  *            they did in the original stream.  This simplifies
41  *            debugging considerably.
42  *  </ul>
43  */
44 public class IndentingReader extends Reader {
45
46     private final Reader r;
47     private final char updent;
48     private final char downdent;
49
50     public IndentingReader(Reader r, char updent, char downdent) {
51         this.r = r;
52         this.updent = updent;
53         this.downdent = downdent;
54         istack.add(0);
55     }
56
57     private boolean indenting = true;
58     private int indentation = 0;
59     private ArrayList<Integer> istack = new ArrayList<Integer>();
60     private ArrayList<Integer> blanks = new ArrayList<Integer>();
61     private int queuedIndentation = 0;
62     private int blankIndentation = 0;
63
64     private int _peek = -2;
65     public int _peek() throws IOException {
66         if (_peek == -2) _peek = r.read();
67         return _peek;
68     }
69     public int _read() throws IOException {
70         int ret = _peek();
71         _peek = -2;
72         return ret;
73     }
74
75     public int read() throws IOException {
76         while(true) {
77             int i = _peek();
78             if (i==-1 && istack.size() > 1) {
79                 istack.remove(istack.size()-1);
80                 return downdent;
81             }
82             if (i==-1) return -1;
83
84             char c = (char)i;
85
86             if (c=='\n') {
87                 if (indenting) {
88                     blanks.add(indentation);
89                 } else {
90                     blanks.add(0);
91                 }
92                 indenting=true;
93                 indentation = 0;
94                 _read();
95                 continue;
96             }
97
98             if (indenting) {
99
100                 // more indentation to consume
101                 if (c==' ') { indentation++; _read(); continue; }
102
103                 // reached end of whitespace; line has non-whitespace material
104                 int last = istack.size()==0 ? -1 : istack.get(istack.size()-1);
105
106                 // emit any required close-braces
107                 if (indentation < last) {
108                     istack.remove(istack.size()-1);
109                     return downdent;
110                 }
111
112                 // emit \n and any blank lines
113                 if (blankIndentation > 0) {
114                     blankIndentation--;
115                     return ' ';
116                 }
117                 if (blanks.size() > 0) {
118                     blankIndentation = blanks.remove(blanks.size()-1);
119                     return '\n';
120                 }
121
122                 if (queuedIndentation < indentation) {
123                     queuedIndentation++;
124
125                     // omit the last space of indentation if we're planning on emitting an open-brace
126                     if (queuedIndentation < indentation || !(indentation > last))
127                         return ' ';
128                 }
129
130                 // emit open-brace
131                 if (indentation > last) {
132                     istack.add(indentation);
133                     return updent;
134                 }
135
136                 // done with indentation
137                 indenting = false;
138                 queuedIndentation = 0;
139             }
140             return _read();
141         }
142     }
143
144     public int read(char[] buf, int off, int len) throws IOException {
145         int numread = 0;
146         while(len>0) {
147             int ret = read();
148             if (ret==-1) break;
149             buf[off] = (char)ret;
150             off++;
151             len--;
152             numread++;
153         }
154         return (len>0 && numread==0) ? -1 : numread;
155     }
156     public void close() throws IOException { r.close(); }
157 }
158