1# The MIT License (MIT) 2# 3# Copyright (c) 2007-2018 Einar Lielmanis, Liam Newman, and contributors. 4# 5# Permission is hereby granted, free of charge, to any person 6# obtaining a copy of this software and associated documentation files 7# (the "Software"), to deal in the Software without restriction, 8# including without limitation the rights to use, copy, modify, merge, 9# publish, distribute, sublicense, and/or sell copies of the Software, 10# and to permit persons to whom the Software is furnished to do so, 11# subject to the following conditions: 12# 13# The above copyright notice and this permission notice shall be 14# included in all copies or substantial portions of the Software. 15# 16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 20# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 21# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23# SOFTWARE. 24 25import re 26import math 27 28# Using object instead of string to allow for later expansion of info 29# about each line 30 31__all__ = ["Output"] 32 33 34class OutputLine: 35 def __init__(self, parent): 36 self.__parent = parent 37 self.__character_count = 0 38 self.__indent_count = -1 39 self.__alignment_count = 0 40 self.__wrap_point_index = 0 41 self.__wrap_point_character_count = 0 42 self.__wrap_point_indent_count = -1 43 self.__wrap_point_alignment_count = 0 44 45 self.__items = [] 46 47 def clone_empty(self): 48 line = OutputLine(self.__parent) 49 line.set_indent(self.__indent_count, self.__alignment_count) 50 return line 51 52 def item(self, index): 53 return self.__items[index] 54 55 def is_empty(self): 56 return len(self.__items) == 0 57 58 def set_indent(self, indent=0, alignment=0): 59 if self.is_empty(): 60 self.__indent_count = indent 61 self.__alignment_count = alignment 62 self.__character_count = self.__parent.get_indent_size( 63 self.__indent_count, self.__alignment_count) 64 65 def _set_wrap_point(self): 66 if self.__parent.wrap_line_length: 67 self.__wrap_point_index = len(self.__items) 68 self.__wrap_point_character_count = self.__character_count 69 self.__wrap_point_indent_count = \ 70 self.__parent.next_line.__indent_count 71 self.__wrap_point_alignment_count = \ 72 self.__parent.next_line.__alignment_count 73 74 def _should_wrap(self): 75 return self.__wrap_point_index and \ 76 self.__character_count > \ 77 self.__parent.wrap_line_length and \ 78 self.__wrap_point_character_count > \ 79 self.__parent.next_line.__character_count 80 81 82 def _allow_wrap(self): 83 if self._should_wrap(): 84 self.__parent.add_new_line() 85 next = self.__parent.current_line 86 next.set_indent(self.__wrap_point_indent_count, 87 self.__wrap_point_alignment_count) 88 next.__items = self.__items[self.__wrap_point_index:] 89 self.__items = self.__items[:self.__wrap_point_index] 90 91 next.__character_count += self.__character_count - \ 92 self.__wrap_point_character_count 93 self.__character_count = self.__wrap_point_character_count 94 95 if next.__items[0] == " ": 96 next.__items.pop(0) 97 next.__character_count -= 1 98 99 return True 100 101 return False 102 103 def last(self): 104 if not self.is_empty(): 105 return self.__items[-1] 106 107 return None 108 109 def push(self, item): 110 self.__items.append(item) 111 last_newline_index = item.rfind('\n') 112 if last_newline_index != -1: 113 self.__character_count = len(item) - last_newline_index 114 else: 115 self.__character_count += len(item) 116 117 def pop(self): 118 item = None 119 if not self.is_empty(): 120 item = self.__items.pop() 121 self.__character_count -= len(item) 122 return item 123 124 def _remove_indent(self): 125 if self.__indent_count > 0: 126 self.__indent_count -= 1 127 self.__character_count -= self.__parent.indent_size 128 129 def _remove_wrap_indent(self): 130 if self.__wrap_point_indent_count > 0: 131 self.__wrap_point_indent_count -= 1 132 133 def trim(self): 134 while self.last() == ' ': 135 self.__items.pop() 136 self.__character_count -= 1 137 138 def toString(self): 139 result = '' 140 if self.is_empty(): 141 if self.__parent.indent_empty_lines: 142 result = self.__parent.get_indent_string(self.__indent_count) 143 else: 144 result = self.__parent.get_indent_string( 145 self.__indent_count, self.__alignment_count) 146 result += ''.join(self.__items) 147 return result 148 149 150class IndentStringCache: 151 def __init__(self, options, base_string): 152 self.__cache = [''] 153 self.__indent_size = options.indent_size 154 self.__indent_string = options.indent_char 155 if not options.indent_with_tabs: 156 self.__indent_string = options.indent_char * options.indent_size 157 158 # Set to null to continue support of auto detection of base indent 159 base_string = base_string or '' 160 if options.indent_level > 0: 161 base_string = options.indent_level * self.__indent_string 162 163 self.__base_string = base_string 164 self.__base_string_length = len(base_string) 165 166 def get_indent_size(self, indent, column=0): 167 result = self.__base_string_length 168 if indent < 0: 169 result = 0 170 result += indent * self.__indent_size 171 result += column 172 return result 173 174 def get_indent_string(self, indent_level, column=0): 175 result = self.__base_string 176 if indent_level < 0: 177 indent_level = 0 178 result = '' 179 column += indent_level * self.__indent_size 180 self.__ensure_cache(column) 181 result += self.__cache[column] 182 return result 183 184 def __ensure_cache(self, column): 185 while column >= len(self.__cache): 186 self.__add_column() 187 188 def __add_column(self): 189 column = len(self.__cache) 190 indent = 0 191 result = '' 192 if self.__indent_size and column >= self.__indent_size: 193 indent = int(math.floor(column / self.__indent_size)) 194 column -= indent * self.__indent_size 195 result = indent * self.__indent_string 196 if column: 197 result += column * ' ' 198 self.__cache.append(result) 199 200 201class Output: 202 def __init__(self, options, baseIndentString=''): 203 204 self.__indent_cache = IndentStringCache(options, baseIndentString) 205 self.raw = False 206 self._end_with_newline = options.end_with_newline 207 self.indent_size = options.indent_size 208 self.wrap_line_length = options.wrap_line_length 209 self.indent_empty_lines = options.indent_empty_lines 210 self.__lines = [] 211 self.previous_line = None 212 self.current_line = None 213 self.next_line = OutputLine(self) 214 self.space_before_token = False 215 self.non_breaking_space = False 216 self.previous_token_wrapped = False 217 # initialize 218 self.__add_outputline() 219 220 def __add_outputline(self): 221 self.previous_line = self.current_line 222 self.current_line = self.next_line.clone_empty() 223 self.__lines.append(self.current_line) 224 225 def get_line_number(self): 226 return len(self.__lines) 227 228 def get_indent_string(self, indent, column=0): 229 return self.__indent_cache.get_indent_string(indent, column) 230 231 def get_indent_size(self, indent, column=0): 232 return self.__indent_cache.get_indent_size(indent, column) 233 234 def is_empty(self): 235 return self.previous_line is None and self.current_line.is_empty() 236 237 def add_new_line(self, force_newline=False): 238 # never newline at the start of file 239 # otherwise, newline only if we didn't just add one or we're forced 240 if self.is_empty() or \ 241 (not force_newline and self.just_added_newline()): 242 return False 243 244 # if raw output is enabled, don't print additional newlines, 245 # but still return True as though you had 246 if not self.raw: 247 self.__add_outputline() 248 return True 249 250 def get_code(self, eol): 251 self.trim(True) 252 253 # handle some edge cases where the last tokens 254 # has text that ends with newline(s) 255 last_item = self.current_line.pop() 256 if last_item: 257 if last_item[-1] == '\n': 258 last_item = re.sub(r'[\n]+$', '', last_item) 259 self.current_line.push(last_item) 260 261 if self._end_with_newline: 262 self.__add_outputline() 263 264 sweet_code = "\n".join(line.toString() for line in self.__lines) 265 266 if not eol == '\n': 267 sweet_code = sweet_code.replace('\n', eol) 268 269 return sweet_code 270 271 def set_wrap_point(self): 272 self.current_line._set_wrap_point() 273 274 def set_indent(self, indent=0, alignment=0): 275 # Next line stores alignment values 276 self.next_line.set_indent(indent, alignment) 277 278 # Never indent your first output indent at the start of the file 279 if len(self.__lines) > 1: 280 self.current_line.set_indent(indent, alignment) 281 return True 282 self.current_line.set_indent() 283 return False 284 285 def add_raw_token(self, token): 286 for _ in range(token.newlines): 287 self.__add_outputline() 288 289 self.current_line.set_indent(-1) 290 self.current_line.push(token.whitespace_before) 291 self.current_line.push(token.text) 292 self.space_before_token = False 293 self.non_breaking_space = False 294 self.previous_token_wrapped = False 295 296 def add_token(self, printable_token): 297 self.__add_space_before_token() 298 self.current_line.push(printable_token) 299 self.space_before_token = False 300 self.non_breaking_space = False 301 self.previous_token_wrapped = self.current_line._allow_wrap() 302 303 def __add_space_before_token(self): 304 if self.space_before_token and not self.just_added_newline(): 305 if not self.non_breaking_space: 306 self.set_wrap_point() 307 self.current_line.push(' ') 308 self.space_before_token = False 309 310 def remove_indent(self, index): 311 while index < len(self.__lines): 312 self.__lines[index]._remove_indent() 313 index += 1 314 self.current_line._remove_wrap_indent() 315 316 def trim(self, eat_newlines=False): 317 self.current_line.trim() 318 319 while eat_newlines and len( 320 self.__lines) > 1 and self.current_line.is_empty(): 321 self.__lines.pop() 322 self.current_line = self.__lines[-1] 323 self.current_line.trim() 324 325 if len(self.__lines) > 1: 326 self.previous_line = self.__lines[-2] 327 else: 328 self.previous_line = None 329 330 def just_added_newline(self): 331 return self.current_line.is_empty() 332 333 def just_added_blankline(self): 334 return self.is_empty() or \ 335 (self.current_line.is_empty() and self.previous_line.is_empty()) 336 337 def ensure_empty_line_above(self, starts_with, ends_with): 338 index = len(self.__lines) - 2 339 while index >= 0: 340 potentialEmptyLine = self.__lines[index] 341 if potentialEmptyLine.is_empty(): 342 break 343 elif not potentialEmptyLine.item(0).startswith(starts_with) and \ 344 potentialEmptyLine.item(-1) != ends_with: 345 self.__lines.insert(index + 1, OutputLine(self)) 346 self.previous_line = self.__lines[-2] 347 break 348 index -= 1 349