PageRenderTime 1310ms CodeModel.GetById 838ms app.highlight 281ms RepoModel.GetById 178ms app.codeStats 0ms

/Lib/formatter.py

http://unladen-swallow.googlecode.com/
Python | 444 lines | 428 code | 1 blank | 15 comment | 4 complexity | 2e8508d0f919e46cc3363a126dd755d0 MD5 | raw file
  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
 22
 23
 24AS_IS = None
 25
 26
 27class 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
 61class 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, (size, i, b, tt)):
232        if self.softspace:
233            self.hard_break = self.para_end = self.softspace = 0
234            self.nospace = 1
235            self.writer.send_flowing_data(' ')
236        if self.font_stack:
237            csize, ci, cb, ctt = self.font_stack[-1]
238            if size is AS_IS: size = csize
239            if i is AS_IS: i = ci
240            if b is AS_IS: b = cb
241            if tt is AS_IS: tt = ctt
242        font = (size, i, b, tt)
243        self.font_stack.append(font)
244        self.writer.new_font(font)
245
246    def pop_font(self):
247        if self.font_stack:
248            del self.font_stack[-1]
249        if self.font_stack:
250            font = self.font_stack[-1]
251        else:
252            font = None
253        self.writer.new_font(font)
254
255    def push_margin(self, margin):
256        self.margin_stack.append(margin)
257        fstack = filter(None, self.margin_stack)
258        if not margin and fstack:
259            margin = fstack[-1]
260        self.writer.new_margin(margin, len(fstack))
261
262    def pop_margin(self):
263        if self.margin_stack:
264            del self.margin_stack[-1]
265        fstack = filter(None, self.margin_stack)
266        if fstack:
267            margin = fstack[-1]
268        else:
269            margin = None
270        self.writer.new_margin(margin, len(fstack))
271
272    def set_spacing(self, spacing):
273        self.spacing = spacing
274        self.writer.new_spacing(spacing)
275
276    def push_style(self, *styles):
277        if self.softspace:
278            self.hard_break = self.para_end = self.softspace = 0
279            self.nospace = 1
280            self.writer.send_flowing_data(' ')
281        for style in styles:
282            self.style_stack.append(style)
283        self.writer.new_styles(tuple(self.style_stack))
284
285    def pop_style(self, n=1):
286        del self.style_stack[-n:]
287        self.writer.new_styles(tuple(self.style_stack))
288
289    def assert_line_data(self, flag=1):
290        self.nospace = self.hard_break = not flag
291        self.para_end = self.parskip = self.have_label = 0
292
293
294class NullWriter:
295    """Minimal writer interface to use in testing & inheritance.
296
297    A writer which only provides the interface definition; no actions are
298    taken on any methods.  This should be the base class for all writers
299    which do not need to inherit any implementation methods.
300
301    """
302    def __init__(self): pass
303    def flush(self): pass
304    def new_alignment(self, align): pass
305    def new_font(self, font): pass
306    def new_margin(self, margin, level): pass
307    def new_spacing(self, spacing): pass
308    def new_styles(self, styles): pass
309    def send_paragraph(self, blankline): pass
310    def send_line_break(self): pass
311    def send_hor_rule(self, *args, **kw): pass
312    def send_label_data(self, data): pass
313    def send_flowing_data(self, data): pass
314    def send_literal_data(self, data): pass
315
316
317class AbstractWriter(NullWriter):
318    """A writer which can be used in debugging formatters, but not much else.
319
320    Each method simply announces itself by printing its name and
321    arguments on standard output.
322
323    """
324
325    def new_alignment(self, align):
326        print "new_alignment(%r)" % (align,)
327
328    def new_font(self, font):
329        print "new_font(%r)" % (font,)
330
331    def new_margin(self, margin, level):
332        print "new_margin(%r, %d)" % (margin, level)
333
334    def new_spacing(self, spacing):
335        print "new_spacing(%r)" % (spacing,)
336
337    def new_styles(self, styles):
338        print "new_styles(%r)" % (styles,)
339
340    def send_paragraph(self, blankline):
341        print "send_paragraph(%r)" % (blankline,)
342
343    def send_line_break(self):
344        print "send_line_break()"
345
346    def send_hor_rule(self, *args, **kw):
347        print "send_hor_rule()"
348
349    def send_label_data(self, data):
350        print "send_label_data(%r)" % (data,)
351
352    def send_flowing_data(self, data):
353        print "send_flowing_data(%r)" % (data,)
354
355    def send_literal_data(self, data):
356        print "send_literal_data(%r)" % (data,)
357
358
359class DumbWriter(NullWriter):
360    """Simple writer class which writes output on the file object passed in
361    as the file parameter or, if file is omitted, on standard output.  The
362    output is simply word-wrapped to the number of columns specified by
363    the maxcol parameter.  This class is suitable for reflowing a sequence
364    of paragraphs.
365
366    """
367
368    def __init__(self, file=None, maxcol=72):
369        self.file = file or sys.stdout
370        self.maxcol = maxcol
371        NullWriter.__init__(self)
372        self.reset()
373
374    def reset(self):
375        self.col = 0
376        self.atbreak = 0
377
378    def send_paragraph(self, blankline):
379        self.file.write('\n'*blankline)
380        self.col = 0
381        self.atbreak = 0
382
383    def send_line_break(self):
384        self.file.write('\n')
385        self.col = 0
386        self.atbreak = 0
387
388    def send_hor_rule(self, *args, **kw):
389        self.file.write('\n')
390        self.file.write('-'*self.maxcol)
391        self.file.write('\n')
392        self.col = 0
393        self.atbreak = 0
394
395    def send_literal_data(self, data):
396        self.file.write(data)
397        i = data.rfind('\n')
398        if i >= 0:
399            self.col = 0
400            data = data[i+1:]
401        data = data.expandtabs()
402        self.col = self.col + len(data)
403        self.atbreak = 0
404
405    def send_flowing_data(self, data):
406        if not data: return
407        atbreak = self.atbreak or data[0].isspace()
408        col = self.col
409        maxcol = self.maxcol
410        write = self.file.write
411        for word in data.split():
412            if atbreak:
413                if col + len(word) >= maxcol:
414                    write('\n')
415                    col = 0
416                else:
417                    write(' ')
418                    col = col + 1
419            write(word)
420            col = col + len(word)
421            atbreak = 1
422        self.col = col
423        self.atbreak = data[-1].isspace()
424
425
426def test(file = None):
427    w = DumbWriter()
428    f = AbstractFormatter(w)
429    if file is not None:
430        fp = open(file)
431    elif sys.argv[1:]:
432        fp = open(sys.argv[1])
433    else:
434        fp = sys.stdin
435    for line in fp:
436        if line == '\n':
437            f.end_paragraph(1)
438        else:
439            f.add_flowing_data(line)
440    f.end_paragraph(0)
441
442
443if __name__ == '__main__':
444    test()