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