1# vim:fileencoding=utf-8:noet 2from __future__ import (unicode_literals, division, absolute_import, print_function) 3 4import sys 5import re 6 7from powerline.lib.encoding import get_preferred_output_encoding 8 9 10NON_PRINTABLE_STR = ( 11 '[^' 12 # ASCII control characters: 0x00-0x19 13 + '\t\n' # Tab, newline: allowed ASCII control characters 14 + '\x20-\x7E' # ASCII printable characters 15 # Unicode control characters: 0x7F-0x9F 16 + '\u0085' # Allowed unicode control character: next line character 17 + '\u00A0-\uD7FF' 18 # Surrogate escapes: 0xD800-0xDFFF 19 + '\uE000-\uFFFD' 20 + (( 21 '\uD800-\uDFFF' 22 ) if sys.maxunicode < 0x10FFFF else ( 23 '\U00010000-\U0010FFFF' 24 )) 25 + ']' 26 + (( 27 # Paired surrogate escapes: allowed in UCS-2 builds as the only way to 28 # represent characters above 0xFFFF. Only paired variant is allowed. 29 '|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]' 30 + '|[\uD800-\uDBFF](?![\uDC00-\uDFFF])' 31 ) if sys.maxunicode < 0x10FFFF else ( 32 '' 33 )) 34) 35NON_PRINTABLE_RE = re.compile(NON_PRINTABLE_STR) 36 37 38def repl(s): 39 return '<x%04x>' % ord(s.group()) 40 41 42def strtrans(s): 43 return NON_PRINTABLE_RE.sub(repl, s.replace('\t', '>---')) 44 45 46class Mark: 47 def __init__(self, name, line, column, buffer, pointer, old_mark=None, merged_marks=None): 48 self.name = name 49 self.line = line 50 self.column = column 51 self.buffer = buffer 52 self.pointer = pointer 53 self.old_mark = old_mark 54 self.merged_marks = merged_marks or [] 55 56 def copy(self): 57 return Mark(self.name, self.line, self.column, self.buffer, self.pointer, self.old_mark, self.merged_marks[:]) 58 59 def get_snippet(self, indent=4, max_length=75): 60 if self.buffer is None: 61 return None 62 head = '' 63 start = self.pointer 64 while start > 0 and self.buffer[start - 1] not in '\0\n': 65 start -= 1 66 if self.pointer - start > max_length / 2 - 1: 67 head = ' ... ' 68 start += 5 69 break 70 tail = '' 71 end = self.pointer 72 while end < len(self.buffer) and self.buffer[end] not in '\0\n': 73 end += 1 74 if end - self.pointer > max_length / 2 - 1: 75 tail = ' ... ' 76 end -= 5 77 break 78 snippet = [self.buffer[start:self.pointer], self.buffer[self.pointer], self.buffer[self.pointer + 1:end]] 79 snippet = [strtrans(s) for s in snippet] 80 return ( 81 ' ' * indent + head + ''.join(snippet) + tail + '\n' 82 + ' ' * (indent + len(head) + len(snippet[0])) + '^' 83 ) 84 85 def advance_string(self, diff): 86 ret = self.copy() 87 # FIXME Currently does not work properly with escaped strings. 88 ret.column += diff 89 ret.pointer += diff 90 return ret 91 92 def set_old_mark(self, old_mark): 93 if self is old_mark: 94 return 95 checked_marks = set([id(self)]) 96 older_mark = old_mark 97 while True: 98 if id(older_mark) in checked_marks: 99 raise ValueError('Trying to set recursive marks') 100 checked_marks.add(id(older_mark)) 101 older_mark = older_mark.old_mark 102 if not older_mark: 103 break 104 self.old_mark = old_mark 105 106 def set_merged_mark(self, merged_mark): 107 self.merged_marks.append(merged_mark) 108 109 def to_string(self, indent=0, head_text='in ', add_snippet=True): 110 mark = self 111 where = '' 112 processed_marks = set() 113 while mark: 114 indentstr = ' ' * indent 115 where += ('%s %s"%s", line %d, column %d' % ( 116 indentstr, head_text, mark.name, mark.line + 1, mark.column + 1)) 117 if add_snippet: 118 snippet = mark.get_snippet(indent=(indent + 4)) 119 if snippet: 120 where += ':\n' + snippet 121 if mark.merged_marks: 122 where += '\n' + indentstr + ' with additionally merged\n' 123 where += mark.merged_marks[0].to_string(indent + 4, head_text='', add_snippet=False) 124 for mmark in mark.merged_marks[1:]: 125 where += '\n' + indentstr + ' and\n' 126 where += mmark.to_string(indent + 4, head_text='', add_snippet=False) 127 if add_snippet: 128 processed_marks.add(id(mark)) 129 if mark.old_mark: 130 where += '\n' + indentstr + ' which replaced value\n' 131 indent += 4 132 mark = mark.old_mark 133 if id(mark) in processed_marks: 134 raise ValueError('Trying to dump recursive mark') 135 return where 136 137 if sys.version_info < (3,): 138 def __str__(self): 139 return self.to_string().encode('utf-8') 140 141 def __unicode__(self): 142 return self.to_string() 143 else: 144 def __str__(self): 145 return self.to_string() 146 147 def __eq__(self, other): 148 return self is other or ( 149 self.name == other.name 150 and self.line == other.line 151 and self.column == other.column 152 ) 153 154 155if sys.version_info < (3,): 156 def echoerr(**kwargs): 157 stream = kwargs.pop('stream', sys.stderr) 158 stream.write('\n') 159 stream.write((format_error(**kwargs) + '\n').encode(get_preferred_output_encoding())) 160else: 161 def echoerr(**kwargs): 162 stream = kwargs.pop('stream', sys.stderr) 163 stream.write('\n') 164 stream.write(format_error(**kwargs) + '\n') 165 166 167def format_error(context=None, context_mark=None, problem=None, problem_mark=None, note=None, indent=0): 168 lines = [] 169 indentstr = ' ' * indent 170 if context is not None: 171 lines.append(indentstr + context) 172 if ( 173 context_mark is not None 174 and ( 175 problem is None or problem_mark is None 176 or context_mark != problem_mark 177 ) 178 ): 179 lines.append(context_mark.to_string(indent=indent)) 180 if problem is not None: 181 lines.append(indentstr + problem) 182 if problem_mark is not None: 183 lines.append(problem_mark.to_string(indent=indent)) 184 if note is not None: 185 lines.append(indentstr + note) 186 return '\n'.join(lines) 187 188 189class MarkedError(Exception): 190 def __init__(self, context=None, context_mark=None, problem=None, problem_mark=None, note=None): 191 Exception.__init__(self, format_error(context, context_mark, problem, problem_mark, note)) 192 193 194class EchoErr(object): 195 __slots__ = ('echoerr', 'logger', 'indent') 196 197 def __init__(self, echoerr, logger, indent=0): 198 self.echoerr = echoerr 199 self.logger = logger 200 self.indent = indent 201 202 def __call__(self, **kwargs): 203 kwargs = kwargs.copy() 204 kwargs.setdefault('indent', self.indent) 205 self.echoerr(**kwargs) 206 207 208class DelayedEchoErr(EchoErr): 209 __slots__ = ('echoerr', 'logger', 'errs', 'message', 'separator_message', 'indent', 'indent_shift') 210 211 def __init__(self, echoerr, message='', separator_message=''): 212 super(DelayedEchoErr, self).__init__(echoerr, echoerr.logger) 213 self.errs = [[]] 214 self.message = message 215 self.separator_message = separator_message 216 self.indent_shift = (4 if message or separator_message else 0) 217 self.indent = echoerr.indent + self.indent_shift 218 219 def __call__(self, **kwargs): 220 kwargs = kwargs.copy() 221 kwargs['indent'] = kwargs.get('indent', 0) + self.indent 222 self.errs[-1].append(kwargs) 223 224 def next_variant(self): 225 self.errs.append([]) 226 227 def echo_all(self): 228 if self.message: 229 self.echoerr(problem=self.message, indent=(self.indent - self.indent_shift)) 230 for variant in self.errs: 231 if not variant: 232 continue 233 if self.separator_message and variant is not self.errs[0]: 234 self.echoerr(problem=self.separator_message, indent=(self.indent - self.indent_shift)) 235 for kwargs in variant: 236 self.echoerr(**kwargs) 237 238 def __nonzero__(self): 239 return not not self.errs 240 241 __bool__ = __nonzero__ 242