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