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