001/*
002 * Copyright (C) 2009-2011 Mathias Doenitz
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.parboiled.buffers;
018
019import org.parboiled.common.IntArrayStack;
020import org.parboiled.errors.IllegalIndentationException;
021import org.parboiled.support.Chars;
022import org.parboiled.support.IndexRange;
023import org.parboiled.support.Position;
024import org.parboiled.common.IntArrayStack;
025import org.parboiled.common.Preconditions;
026import org.parboiled.errors.IllegalIndentationException;
027import org.parboiled.support.Chars;
028import org.parboiled.support.IndexRange;
029import org.parboiled.support.Position;
030
031import static org.parboiled.common.Preconditions.checkArgument;
032
033/**
034 * Special, immutable InputBuffer implementation for indentation based grammars.
035 * <p>This InputBuffer collapses all space and tab characters at the beginning of a text line into either nothing (if
036 * the line has the same indentation level as the previous line), a special {@link Chars#INDENT} character (if the line
037 * has a greater indentation level than the previous line) or one or more {@link Chars#DEDENT} characters (if the line
038 * has a lower indentation level than the previous line).</p>
039 * <p>Blank lines (lines containing nothing but whitespace) are removed from the input and the buffer can, optionally,
040 * remove line comments (i.e. comments that start with a predefined character sequence and go to the end of the line).
041 * </p>
042 * <p>This means that the highest index of this InputBuffer is probably smaller than that of the original input text
043 * buffer, since all line indentations and blank lines have been collapsed. However, the implementation will make sure
044 * that {@link #getPosition(int)}, {@link #extract(int, int)}, etc. will work as expected and always return the
045 * "correct" result from the underlying, original input buffer.</p>
046 * <p>If the input contains illegal indentation the buffer throws an {@link IllegalIndentationException}
047 * during construction</p>
048 */
049public class IndentDedentInputBuffer implements InputBuffer {
050    private final DefaultInputBuffer origBuffer;
051    private final DefaultInputBuffer convBuffer;
052
053    private int[] indexMap; // maps convBuffer indices to origBuffer indices
054    private final boolean strict;
055    private final boolean skipEmptyLines;
056
057    /**
058     * Creates a new IndentDedentInputBuffer around the given char array. Note that for performance reasons the given
059     * char array is not defensively copied.
060     *
061     * @param input            the input text.
062     * @param tabStop          the number of characters in a tab stop.
063     * @param lineCommentStart the string starting a line comment or null, if line comments are not defined
064     * @param strict           signals whether the buffer should throw an {@link IllegalIndentationException} on
065     * "semi-dedents", if false the buffer silently accepts these
066     * @throws IllegalIndentationException
067     *          if the input contains illegal indentations and the strict flag is set
068     */
069    public IndentDedentInputBuffer(char[] input, int tabStop, String lineCommentStart, boolean strict) {
070        this(input, tabStop, lineCommentStart, strict, true);
071    }
072
073    /**
074     * Creates a new IndentDedentInputBuffer around the given char array. Note that for performance reasons the given
075     * char array is not defensively copied.
076     *
077     * @param input            the input text.
078     * @param tabStop          the number of characters in a tab stop.
079     * @param lineCommentStart the string starting a line comment or null, if line comments are not defined
080     * @param strict           signals whether the buffer should throw an {@link IllegalIndentationException} on
081     * "semi-dedents", if false the buffer silently accepts these
082     * @param skipEmptyLines   signals whether the buffer should swallow empty lines
083     * @throws IllegalIndentationException
084     *          if the input contains illegal indentations and the strict flag is set
085     */
086    public IndentDedentInputBuffer(char[] input, int tabStop, String lineCommentStart, boolean strict,
087                                   boolean skipEmptyLines) {
088        this.strict = strict;
089        this.skipEmptyLines = skipEmptyLines;
090        Preconditions.checkArgument(tabStop > 0, "tabStop must be > 0");
091        Preconditions.checkArgument(lineCommentStart == null || lineCommentStart.indexOf('\n') == -1,
092                "lineCommentStart must not contain newlines");
093        origBuffer = new DefaultInputBuffer(input);
094        BufferConverter converter = new BufferConverter(tabStop,
095                lineCommentStart != null ? lineCommentStart.toCharArray() : null);
096        convBuffer = new DefaultInputBuffer(converter.builder.getChars());
097        indexMap = converter.builder.getIndexMap();
098    }
099
100    public char charAt(int index) {
101        return convBuffer.charAt(index);
102    }
103
104    public boolean test(int index, char[] characters) {
105        return convBuffer.test(index, characters);
106    }
107
108    public String extract(int start, int end) {
109        return origBuffer.extract(map(start), map(end));
110    }
111
112    public String extract(IndexRange range) {
113        return origBuffer.extract(map(range.start), map(range.end));
114    }
115
116    public Position getPosition(int index) {
117        return origBuffer.getPosition(map(index));
118    }
119
120    public int getOriginalIndex(int index) {
121        return map(index);
122    }
123
124    public String extractLine(int lineNumber) {
125        return origBuffer.extractLine(lineNumber);
126    }
127
128    public int getLineCount() {return origBuffer.getLineCount();}
129
130    private int map(int convIndex) {
131        if (convIndex < 0) return indexMap[0];
132        if (convIndex < indexMap.length) return indexMap[convIndex];
133        if (indexMap.length == 0) return 1;
134        return indexMap[indexMap.length - 1] + 1;
135    }
136
137    private class BufferConverter {
138        public final BufferBuilder builder = new BufferBuilder();
139        private final int tabStop;
140        private final char[] lineCommentStart;
141        private final IntArrayStack previousLevels = new IntArrayStack();
142        private int cursor = 0;
143        private char currentChar;
144
145        public BufferConverter(int tabStop, char[] lineCommentStart) {
146            this.tabStop = tabStop;
147            this.lineCommentStart = lineCommentStart;
148            this.currentChar = origBuffer.charAt(0);
149            build();
150        }
151
152        private void build() {
153            previousLevels.push(0);
154
155            // consume inital indent
156            int currentLevel = skipIndent();
157
158            // transform all other input
159            while (currentChar != Chars.EOI) {
160                int commentChars = skipLineComment();
161                if (currentChar != '\n' && currentChar != Chars.EOI) {
162                    builder.append(currentChar);
163                    advance();
164                    continue;
165                }
166
167                // register newline
168                builder.appendNewline(commentChars);
169                advance();
170
171                // consume line indent
172                int indent = skipIndent();
173
174                // generate INDENTS/DEDENTS
175                if (indent > currentLevel) {
176                    previousLevels.push(currentLevel);
177                    currentLevel = indent;
178                    builder.append(Chars.INDENT);
179                } else {
180                    while (indent < currentLevel && indent <= previousLevels.peek()) {
181                        currentLevel = previousLevels.pop();
182                        builder.append(Chars.DEDENT);
183                    }
184                    if (strict && indent < currentLevel) {
185                        throw new IllegalIndentationException(origBuffer, origBuffer.getPosition(cursor));
186                    }
187                }
188            }
189
190            // make sure to close all remaining indentation scopes
191            if (previousLevels.size() > 1) {
192                builder.append('\n');
193                while (previousLevels.size() > 1) {
194                    previousLevels.pop();
195                    builder.append(Chars.DEDENT);
196                }
197            }
198        }
199
200        private int skipIndent() {
201            int indent = 0;
202            loop:
203            while (true) {
204                switch (currentChar) {
205                    case ' ':
206                        indent++;
207                        advance();
208                        continue;
209                    case '\t':
210                        indent = ((indent / tabStop) + 1) * tabStop;
211                        advance();
212                        continue;
213                    case '\n':
214                        if (!skipEmptyLines) builder.appendNewline(0);
215                        indent = 0;
216                        advance();
217                        continue;
218                    case Chars.EOI:
219                        indent = 0;
220                        break loop;
221                    default:
222                        if (skipLineComment() == 0) break loop;
223                }
224            }
225            return indent;
226        }
227
228        private void advance() {
229            currentChar = origBuffer.charAt(++cursor);
230        }
231
232        private int skipLineComment() {
233            if (lineCommentStart != null && origBuffer.test(cursor, lineCommentStart)) {
234                int start = cursor;
235                while (currentChar != '\n' && currentChar != Chars.EOI) {
236                    advance();
237                }
238                return cursor - start;
239            }
240            return 0;
241        }
242
243        private class BufferBuilder {
244            private final StringBuilder sb = new StringBuilder();
245            private final IntArrayStack indexMap = new IntArrayStack();
246
247            private void append(char c) {
248                indexMap.push(cursor);
249                sb.append(c);
250            }
251
252            private void appendNewline(int commentChars) {
253                indexMap.push(cursor - commentChars);
254                sb.append('\n');
255            }
256
257            public char[] getChars() {
258                char[] buffer = new char[sb.length()];
259                sb.getChars(0, sb.length(), buffer, 0);
260                return buffer;
261            }
262
263            public int[] getIndexMap() {
264                return indexMap.toArray();
265            }
266        }
267    }
268}
269