1"""Generic output formatting. 2 3Formatter objects transform an abstract flow of formatting events into 4specific output events on writer objects. Formatters manage several stack 5structures to allow various properties of a writer object to be changed and 6restored; writers need not be able to handle relative changes nor any sort 7of ``change back'' operation. Specific writer properties which may be 8controlled via formatter objects are horizontal alignment, font, and left 9margin indentations. A mechanism is provided which supports providing 10arbitrary, non-exclusive style settings to a writer as well. Additional 11interfaces facilitate formatting events which are not reversible, such as 12paragraph separation. 13 14Writer objects encapsulate device interfaces. Abstract devices, such as 15file formats, are supported as well as physical devices. The provided 16implementations all work with abstract devices. The interface makes 17available mechanisms for setting the properties which formatter objects 18manage and inserting data into the output. 19""" 20 21import sys 22import warnings 23warnings.warn('the formatter module is deprecated', DeprecationWarning, 24 stacklevel=2) 25 26 27AS_IS = None 28 29 30class NullFormatter: 31 """A formatter which does nothing. 32 33 If the writer parameter is omitted, a NullWriter instance is created. 34 No methods of the writer are called by NullFormatter instances. 35 36 Implementations should inherit from this class if implementing a writer 37 interface but don't need to inherit any implementation. 38 39 """ 40 41 def __init__(self, writer=None): 42 if writer is None: 43 writer = NullWriter() 44 self.writer = writer 45 def end_paragraph(self, blankline): pass 46 def add_line_break(self): pass 47 def add_hor_rule(self, *args, **kw): pass 48 def add_label_data(self, format, counter, blankline=None): pass 49 def add_flowing_data(self, data): pass 50 def add_literal_data(self, data): pass 51 def flush_softspace(self): pass 52 def push_alignment(self, align): pass 53 def pop_alignment(self): pass 54 def push_font(self, x): pass 55 def pop_font(self): pass 56 def push_margin(self, margin): pass 57 def pop_margin(self): pass 58 def set_spacing(self, spacing): pass 59 def push_style(self, *styles): pass 60 def pop_style(self, n=1): pass 61 def assert_line_data(self, flag=1): pass 62 63 64class AbstractFormatter: 65 """The standard formatter. 66 67 This implementation has demonstrated wide applicability to many writers, 68 and may be used directly in most circumstances. It has been used to 69 implement a full-featured World Wide Web browser. 70 71 """ 72 73 # Space handling policy: blank spaces at the boundary between elements 74 # are handled by the outermost context. "Literal" data is not checked 75 # to determine context, so spaces in literal data are handled directly 76 # in all circumstances. 77 78 def __init__(self, writer): 79 self.writer = writer # Output device 80 self.align = None # Current alignment 81 self.align_stack = [] # Alignment stack 82 self.font_stack = [] # Font state 83 self.margin_stack = [] # Margin state 84 self.spacing = None # Vertical spacing state 85 self.style_stack = [] # Other state, e.g. color 86 self.nospace = 1 # Should leading space be suppressed 87 self.softspace = 0 # Should a space be inserted 88 self.para_end = 1 # Just ended a paragraph 89 self.parskip = 0 # Skipped space between paragraphs? 90 self.hard_break = 1 # Have a hard break 91 self.have_label = 0 92 93 def end_paragraph(self, blankline): 94 if not self.hard_break: 95 self.writer.send_line_break() 96 self.have_label = 0 97 if self.parskip < blankline and not self.have_label: 98 self.writer.send_paragraph(blankline - self.parskip) 99 self.parskip = blankline 100 self.have_label = 0 101 self.hard_break = self.nospace = self.para_end = 1 102 self.softspace = 0 103 104 def add_line_break(self): 105 if not (self.hard_break or self.para_end): 106 self.writer.send_line_break() 107 self.have_label = self.parskip = 0 108 self.hard_break = self.nospace = 1 109 self.softspace = 0 110 111 def add_hor_rule(self, *args, **kw): 112 if not self.hard_break: 113 self.writer.send_line_break() 114 self.writer.send_hor_rule(*args, **kw) 115 self.hard_break = self.nospace = 1 116 self.have_label = self.para_end = self.softspace = self.parskip = 0 117 118 def add_label_data(self, format, counter, blankline = None): 119 if self.have_label or not self.hard_break: 120 self.writer.send_line_break() 121 if not self.para_end: 122 self.writer.send_paragraph((blankline and 1) or 0) 123 if isinstance(format, str): 124 self.writer.send_label_data(self.format_counter(format, counter)) 125 else: 126 self.writer.send_label_data(format) 127 self.nospace = self.have_label = self.hard_break = self.para_end = 1 128 self.softspace = self.parskip = 0 129 130 def format_counter(self, format, counter): 131 label = '' 132 for c in format: 133 if c == '1': 134 label = label + ('%d' % counter) 135 elif c in 'aA': 136 if counter > 0: 137 label = label + self.format_letter(c, counter) 138 elif c in 'iI': 139 if counter > 0: 140 label = label + self.format_roman(c, counter) 141 else: 142 label = label + c 143 return label 144 145 def format_letter(self, case, counter): 146 label = '' 147 while counter > 0: 148 counter, x = divmod(counter-1, 26) 149 # This makes a strong assumption that lowercase letters 150 # and uppercase letters form two contiguous blocks, with 151 # letters in order! 152 s = chr(ord(case) + x) 153 label = s + label 154 return label 155 156 def format_roman(self, case, counter): 157 ones = ['i', 'x', 'c', 'm'] 158 fives = ['v', 'l', 'd'] 159 label, index = '', 0 160 # This will die of IndexError when counter is too big 161 while counter > 0: 162 counter, x = divmod(counter, 10) 163 if x == 9: 164 label = ones[index] + ones[index+1] + label 165 elif x == 4: 166 label = ones[index] + fives[index] + label 167 else: 168 if x >= 5: 169 s = fives[index] 170 x = x-5 171 else: 172 s = '' 173 s = s + ones[index]*x 174 label = s + label 175 index = index + 1 176 if case == 'I': 177 return label.upper() 178 return label 179 180 def add_flowing_data(self, data): 181 if not data: return 182 prespace = data[:1].isspace() 183 postspace = data[-1:].isspace() 184 data = " ".join(data.split()) 185 if self.nospace and not data: 186 return 187 elif prespace or self.softspace: 188 if not data: 189 if not self.nospace: 190 self.softspace = 1 191 self.parskip = 0 192 return 193 if not self.nospace: 194 data = ' ' + data 195 self.hard_break = self.nospace = self.para_end = \ 196 self.parskip = self.have_label = 0 197 self.softspace = postspace 198 self.writer.send_flowing_data(data) 199 200 def add_literal_data(self, data): 201 if not data: return 202 if self.softspace: 203 self.writer.send_flowing_data(" ") 204 self.hard_break = data[-1:] == '\n' 205 self.nospace = self.para_end = self.softspace = \ 206 self.parskip = self.have_label = 0 207 self.writer.send_literal_data(data) 208 209 def flush_softspace(self): 210 if self.softspace: 211 self.hard_break = self.para_end = self.parskip = \ 212 self.have_label = self.softspace = 0 213 self.nospace = 1 214 self.writer.send_flowing_data(' ') 215 216 def push_alignment(self, align): 217 if align and align != self.align: 218 self.writer.new_alignment(align) 219 self.align = align 220 self.align_stack.append(align) 221 else: 222 self.align_stack.append(self.align) 223 224 def pop_alignment(self): 225 if self.align_stack: 226 del self.align_stack[-1] 227 if self.align_stack: 228 self.align = align = self.align_stack[-1] 229 self.writer.new_alignment(align) 230 else: 231 self.align = None 232 self.writer.new_alignment(None) 233 234 def push_font(self, font): 235 size, i, b, tt = font 236 if self.softspace: 237 self.hard_break = self.para_end = self.softspace = 0 238 self.nospace = 1 239 self.writer.send_flowing_data(' ') 240 if self.font_stack: 241 csize, ci, cb, ctt = self.font_stack[-1] 242 if size is AS_IS: size = csize 243 if i is AS_IS: i = ci 244 if b is AS_IS: b = cb 245 if tt is AS_IS: tt = ctt 246 font = (size, i, b, tt) 247 self.font_stack.append(font) 248 self.writer.new_font(font) 249 250 def pop_font(self): 251 if self.font_stack: 252 del self.font_stack[-1] 253 if self.font_stack: 254 font = self.font_stack[-1] 255 else: 256 font = None 257 self.writer.new_font(font) 258 259 def push_margin(self, margin): 260 self.margin_stack.append(margin) 261 fstack = [m for m in self.margin_stack if m] 262 if not margin and fstack: 263 margin = fstack[-1] 264 self.writer.new_margin(margin, len(fstack)) 265 266 def pop_margin(self): 267 if self.margin_stack: 268 del self.margin_stack[-1] 269 fstack = [m for m in self.margin_stack if m] 270 if fstack: 271 margin = fstack[-1] 272 else: 273 margin = None 274 self.writer.new_margin(margin, len(fstack)) 275 276 def set_spacing(self, spacing): 277 self.spacing = spacing 278 self.writer.new_spacing(spacing) 279 280 def push_style(self, *styles): 281 if self.softspace: 282 self.hard_break = self.para_end = self.softspace = 0 283 self.nospace = 1 284 self.writer.send_flowing_data(' ') 285 for style in styles: 286 self.style_stack.append(style) 287 self.writer.new_styles(tuple(self.style_stack)) 288 289 def pop_style(self, n=1): 290 del self.style_stack[-n:] 291 self.writer.new_styles(tuple(self.style_stack)) 292 293 def assert_line_data(self, flag=1): 294 self.nospace = self.hard_break = not flag 295 self.para_end = self.parskip = self.have_label = 0 296 297 298class NullWriter: 299 """Minimal writer interface to use in testing & inheritance. 300 301 A writer which only provides the interface definition; no actions are 302 taken on any methods. This should be the base class for all writers 303 which do not need to inherit any implementation methods. 304 305 """ 306 def __init__(self): pass 307 def flush(self): pass 308 def new_alignment(self, align): pass 309 def new_font(self, font): pass 310 def new_margin(self, margin, level): pass 311 def new_spacing(self, spacing): pass 312 def new_styles(self, styles): pass 313 def send_paragraph(self, blankline): pass 314 def send_line_break(self): pass 315 def send_hor_rule(self, *args, **kw): pass 316 def send_label_data(self, data): pass 317 def send_flowing_data(self, data): pass 318 def send_literal_data(self, data): pass 319 320 321class AbstractWriter(NullWriter): 322 """A writer which can be used in debugging formatters, but not much else. 323 324 Each method simply announces itself by printing its name and 325 arguments on standard output. 326 327 """ 328 329 def new_alignment(self, align): 330 print("new_alignment(%r)" % (align,)) 331 332 def new_font(self, font): 333 print("new_font(%r)" % (font,)) 334 335 def new_margin(self, margin, level): 336 print("new_margin(%r, %d)" % (margin, level)) 337 338 def new_spacing(self, spacing): 339 print("new_spacing(%r)" % (spacing,)) 340 341 def new_styles(self, styles): 342 print("new_styles(%r)" % (styles,)) 343 344 def send_paragraph(self, blankline): 345 print("send_paragraph(%r)" % (blankline,)) 346 347 def send_line_break(self): 348 print("send_line_break()") 349 350 def send_hor_rule(self, *args, **kw): 351 print("send_hor_rule()") 352 353 def send_label_data(self, data): 354 print("send_label_data(%r)" % (data,)) 355 356 def send_flowing_data(self, data): 357 print("send_flowing_data(%r)" % (data,)) 358 359 def send_literal_data(self, data): 360 print("send_literal_data(%r)" % (data,)) 361 362 363class DumbWriter(NullWriter): 364 """Simple writer class which writes output on the file object passed in 365 as the file parameter or, if file is omitted, on standard output. The 366 output is simply word-wrapped to the number of columns specified by 367 the maxcol parameter. This class is suitable for reflowing a sequence 368 of paragraphs. 369 370 """ 371 372 def __init__(self, file=None, maxcol=72): 373 self.file = file or sys.stdout 374 self.maxcol = maxcol 375 NullWriter.__init__(self) 376 self.reset() 377 378 def reset(self): 379 self.col = 0 380 self.atbreak = 0 381 382 def send_paragraph(self, blankline): 383 self.file.write('\n'*blankline) 384 self.col = 0 385 self.atbreak = 0 386 387 def send_line_break(self): 388 self.file.write('\n') 389 self.col = 0 390 self.atbreak = 0 391 392 def send_hor_rule(self, *args, **kw): 393 self.file.write('\n') 394 self.file.write('-'*self.maxcol) 395 self.file.write('\n') 396 self.col = 0 397 self.atbreak = 0 398 399 def send_literal_data(self, data): 400 self.file.write(data) 401 i = data.rfind('\n') 402 if i >= 0: 403 self.col = 0 404 data = data[i+1:] 405 data = data.expandtabs() 406 self.col = self.col + len(data) 407 self.atbreak = 0 408 409 def send_flowing_data(self, data): 410 if not data: return 411 atbreak = self.atbreak or data[0].isspace() 412 col = self.col 413 maxcol = self.maxcol 414 write = self.file.write 415 for word in data.split(): 416 if atbreak: 417 if col + len(word) >= maxcol: 418 write('\n') 419 col = 0 420 else: 421 write(' ') 422 col = col + 1 423 write(word) 424 col = col + len(word) 425 atbreak = 1 426 self.col = col 427 self.atbreak = data[-1].isspace() 428 429 430def test(file = None): 431 w = DumbWriter() 432 f = AbstractFormatter(w) 433 if file is not None: 434 fp = open(file) 435 elif sys.argv[1:]: 436 fp = open(sys.argv[1]) 437 else: 438 fp = sys.stdin 439 try: 440 for line in fp: 441 if line == '\n': 442 f.end_paragraph(1) 443 else: 444 f.add_flowing_data(line) 445 finally: 446 if fp is not sys.stdin: 447 fp.close() 448 f.end_paragraph(0) 449 450 451if __name__ == '__main__': 452 test() 453