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