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, 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
295class 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
318class 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
360class 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
427def 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
444if __name__ == '__main__':
445    test()
446