formatter.py 14.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
"""Generic output formatting.

Formatter objects transform an abstract flow of formatting events into
specific output events on writer objects. Formatters manage several stack
structures to allow various properties of a writer object to be changed and
restored; writers need not be able to handle relative changes nor any sort
of ``change back'' operation. Specific writer properties which may be
controlled via formatter objects are horizontal alignment, font, and left
margin indentations. A mechanism is provided which supports providing
arbitrary, non-exclusive style settings to a writer as well. Additional
interfaces facilitate formatting events which are not reversible, such as
Tim Peters's avatar
Tim Peters committed
12
paragraph separation.
13 14 15 16 17

Writer objects encapsulate device interfaces. Abstract devices, such as
file formats, are supported as well as physical devices. The provided
implementations all work with abstract devices. The interface makes
available mechanisms for setting the properties which formatter objects
Tim Peters's avatar
Tim Peters committed
18
manage and inserting data into the output.
19 20
"""

Guido van Rossum's avatar
Guido van Rossum committed
21 22 23 24 25 26
import sys


AS_IS = None


Guido van Rossum's avatar
Guido van Rossum committed
27
class NullFormatter:
28 29 30 31 32 33 34 35 36
    """A formatter which does nothing.

    If the writer parameter is omitted, a NullWriter instance is created.
    No methods of the writer are called by NullFormatter instances.

    Implementations should inherit from this class if implementing a writer
    interface but don't need to inherit any implementation.

    """
Guido van Rossum's avatar
Guido van Rossum committed
37

38
    def __init__(self, writer=None):
39
        if writer is None:
40 41
            writer = NullWriter()
        self.writer = writer
Guido van Rossum's avatar
Guido van Rossum committed
42 43
    def end_paragraph(self, blankline): pass
    def add_line_break(self): pass
44 45
    def add_hor_rule(self, *args, **kw): pass
    def add_label_data(self, format, counter, blankline=None): pass
Guido van Rossum's avatar
Guido van Rossum committed
46 47 48
    def add_flowing_data(self, data): pass
    def add_literal_data(self, data): pass
    def flush_softspace(self): pass
49 50
    def push_alignment(self, align): pass
    def pop_alignment(self): pass
Guido van Rossum's avatar
Guido van Rossum committed
51 52 53 54 55
    def push_font(self, x): pass
    def pop_font(self): pass
    def push_margin(self, margin): pass
    def pop_margin(self): pass
    def set_spacing(self, spacing): pass
56 57 58
    def push_style(self, *styles): pass
    def pop_style(self, n=1): pass
    def assert_line_data(self, flag=1): pass
Guido van Rossum's avatar
Guido van Rossum committed
59 60


Guido van Rossum's avatar
Guido van Rossum committed
61
class AbstractFormatter:
62 63 64 65 66 67 68
    """The standard formatter.

    This implementation has demonstrated wide applicability to many writers,
    and may be used directly in most circumstances.  It has been used to
    implement a full-featured World Wide Web browser.

    """
Guido van Rossum's avatar
Guido van Rossum committed
69

70 71 72 73 74
    #  Space handling policy:  blank spaces at the boundary between elements
    #  are handled by the outermost context.  "Literal" data is not checked
    #  to determine context, so spaces in literal data are handled directly
    #  in all circumstances.

Guido van Rossum's avatar
Guido van Rossum committed
75
    def __init__(self, writer):
76 77 78 79 80 81 82 83 84 85 86 87 88
        self.writer = writer            # Output device
        self.align = None               # Current alignment
        self.align_stack = []           # Alignment stack
        self.font_stack = []            # Font state
        self.margin_stack = []          # Margin state
        self.spacing = None             # Vertical spacing state
        self.style_stack = []           # Other state, e.g. color
        self.nospace = 1                # Should leading space be suppressed
        self.softspace = 0              # Should a space be inserted
        self.para_end = 1               # Just ended a paragraph
        self.parskip = 0                # Skipped space between paragraphs?
        self.hard_break = 1             # Have a hard break
        self.have_label = 0
Guido van Rossum's avatar
Guido van Rossum committed
89 90

    def end_paragraph(self, blankline):
91 92 93 94 95 96 97 98 99
        if not self.hard_break:
            self.writer.send_line_break()
            self.have_label = 0
        if self.parskip < blankline and not self.have_label:
            self.writer.send_paragraph(blankline - self.parskip)
            self.parskip = blankline
            self.have_label = 0
        self.hard_break = self.nospace = self.para_end = 1
        self.softspace = 0
Guido van Rossum's avatar
Guido van Rossum committed
100 101

    def add_line_break(self):
102 103 104 105 106
        if not (self.hard_break or self.para_end):
            self.writer.send_line_break()
            self.have_label = self.parskip = 0
        self.hard_break = self.nospace = 1
        self.softspace = 0
Guido van Rossum's avatar
Guido van Rossum committed
107

108
    def add_hor_rule(self, *args, **kw):
109 110
        if not self.hard_break:
            self.writer.send_line_break()
111
        self.writer.send_hor_rule(*args, **kw)
112 113
        self.hard_break = self.nospace = 1
        self.have_label = self.para_end = self.softspace = self.parskip = 0
114 115

    def add_label_data(self, format, counter, blankline = None):
116 117 118 119
        if self.have_label or not self.hard_break:
            self.writer.send_line_break()
        if not self.para_end:
            self.writer.send_paragraph((blankline and 1) or 0)
120
        if isinstance(format, str):
121 122 123 124 125
            self.writer.send_label_data(self.format_counter(format, counter))
        else:
            self.writer.send_label_data(format)
        self.nospace = self.have_label = self.hard_break = self.para_end = 1
        self.softspace = self.parskip = 0
Guido van Rossum's avatar
Guido van Rossum committed
126 127 128 129

    def format_counter(self, format, counter):
        label = ''
        for c in format:
130 131 132 133 134 135 136 137 138
            if c == '1':
                label = label + ('%d' % counter)
            elif c in 'aA':
                if counter > 0:
                    label = label + self.format_letter(c, counter)
            elif c in 'iI':
                if counter > 0:
                    label = label + self.format_roman(c, counter)
            else:
139
                label = label + c
Guido van Rossum's avatar
Guido van Rossum committed
140 141 142
        return label

    def format_letter(self, case, counter):
143 144 145
        label = ''
        while counter > 0:
            counter, x = divmod(counter-1, 26)
146 147 148
            # This makes a strong assumption that lowercase letters
            # and uppercase letters form two contiguous blocks, with
            # letters in order!
149 150 151
            s = chr(ord(case) + x)
            label = s + label
        return label
Guido van Rossum's avatar
Guido van Rossum committed
152 153 154 155

    def format_roman(self, case, counter):
        ones = ['i', 'x', 'c', 'm']
        fives = ['v', 'l', 'd']
156
        label, index = '', 0
157
        # This will die of IndexError when counter is too big
Guido van Rossum's avatar
Guido van Rossum committed
158 159 160
        while counter > 0:
            counter, x = divmod(counter, 10)
            if x == 9:
161
                label = ones[index] + ones[index+1] + label
Guido van Rossum's avatar
Guido van Rossum committed
162
            elif x == 4:
163
                label = ones[index] + fives[index] + label
Guido van Rossum's avatar
Guido van Rossum committed
164 165 166 167 168 169 170
            else:
                if x >= 5:
                    s = fives[index]
                    x = x-5
                else:
                    s = ''
                s = s + ones[index]*x
171
                label = s + label
Guido van Rossum's avatar
Guido van Rossum committed
172
            index = index + 1
173
        if case == 'I':
174
            return label.upper()
Guido van Rossum's avatar
Guido van Rossum committed
175 176
        return label

177
    def add_flowing_data(self, data):
178
        if not data: return
179 180 181
        prespace = data[:1].isspace()
        postspace = data[-1:].isspace()
        data = " ".join(data.split())
182 183 184 185 186 187 188 189 190 191 192 193 194 195
        if self.nospace and not data:
            return
        elif prespace or self.softspace:
            if not data:
                if not self.nospace:
                    self.softspace = 1
                    self.parskip = 0
                return
            if not self.nospace:
                data = ' ' + data
        self.hard_break = self.nospace = self.para_end = \
                          self.parskip = self.have_label = 0
        self.softspace = postspace
        self.writer.send_flowing_data(data)
Guido van Rossum's avatar
Guido van Rossum committed
196 197

    def add_literal_data(self, data):
198 199 200 201 202 203 204
        if not data: return
        if self.softspace:
            self.writer.send_flowing_data(" ")
        self.hard_break = data[-1:] == '\n'
        self.nospace = self.para_end = self.softspace = \
                       self.parskip = self.have_label = 0
        self.writer.send_literal_data(data)
Guido van Rossum's avatar
Guido van Rossum committed
205

206
    def flush_softspace(self):
207 208 209 210 211
        if self.softspace:
            self.hard_break = self.para_end = self.parskip = \
                              self.have_label = self.softspace = 0
            self.nospace = 1
            self.writer.send_flowing_data(' ')
212

213
    def push_alignment(self, align):
214 215 216 217 218 219
        if align and align != self.align:
            self.writer.new_alignment(align)
            self.align = align
            self.align_stack.append(align)
        else:
            self.align_stack.append(self.align)
220 221

    def pop_alignment(self):
222 223 224 225 226 227 228 229
        if self.align_stack:
            del self.align_stack[-1]
        if self.align_stack:
            self.align = align = self.align_stack[-1]
            self.writer.new_alignment(align)
        else:
            self.align = None
            self.writer.new_alignment(None)
230

Guido van Rossum's avatar
Guido van Rossum committed
231
    def push_font(self, (size, i, b, tt)):
232 233 234 235 236 237 238 239 240 241 242 243 244
        if self.softspace:
            self.hard_break = self.para_end = self.softspace = 0
            self.nospace = 1
            self.writer.send_flowing_data(' ')
        if self.font_stack:
            csize, ci, cb, ctt = self.font_stack[-1]
            if size is AS_IS: size = csize
            if i is AS_IS: i = ci
            if b is AS_IS: b = cb
            if tt is AS_IS: tt = ctt
        font = (size, i, b, tt)
        self.font_stack.append(font)
        self.writer.new_font(font)
Guido van Rossum's avatar
Guido van Rossum committed
245 246

    def pop_font(self):
247 248 249 250 251 252 253
        if self.font_stack:
            del self.font_stack[-1]
        if self.font_stack:
            font = self.font_stack[-1]
        else:
            font = None
        self.writer.new_font(font)
Guido van Rossum's avatar
Guido van Rossum committed
254 255

    def push_margin(self, margin):
256 257 258 259 260
        self.margin_stack.append(margin)
        fstack = filter(None, self.margin_stack)
        if not margin and fstack:
            margin = fstack[-1]
        self.writer.new_margin(margin, len(fstack))
Guido van Rossum's avatar
Guido van Rossum committed
261 262

    def pop_margin(self):
263 264 265 266 267 268 269 270
        if self.margin_stack:
            del self.margin_stack[-1]
        fstack = filter(None, self.margin_stack)
        if fstack:
            margin = fstack[-1]
        else:
            margin = None
        self.writer.new_margin(margin, len(fstack))
Guido van Rossum's avatar
Guido van Rossum committed
271 272

    def set_spacing(self, spacing):
273 274
        self.spacing = spacing
        self.writer.new_spacing(spacing)
Guido van Rossum's avatar
Guido van Rossum committed
275

276
    def push_style(self, *styles):
277 278 279 280 281 282 283
        if self.softspace:
            self.hard_break = self.para_end = self.softspace = 0
            self.nospace = 1
            self.writer.send_flowing_data(' ')
        for style in styles:
            self.style_stack.append(style)
        self.writer.new_styles(tuple(self.style_stack))
Guido van Rossum's avatar
Guido van Rossum committed
284

285
    def pop_style(self, n=1):
286 287
        del self.style_stack[-n:]
        self.writer.new_styles(tuple(self.style_stack))
Guido van Rossum's avatar
Guido van Rossum committed
288

289
    def assert_line_data(self, flag=1):
290 291
        self.nospace = self.hard_break = not flag
        self.para_end = self.parskip = self.have_label = 0
292 293 294


class NullWriter:
295 296 297 298 299 300 301
    """Minimal writer interface to use in testing & inheritance.

    A writer which only provides the interface definition; no actions are
    taken on any methods.  This should be the base class for all writers
    which do not need to inherit any implementation methods.

    """
302
    def __init__(self): pass
303
    def flush(self): pass
304 305 306 307 308 309 310 311 312 313 314 315
    def new_alignment(self, align): pass
    def new_font(self, font): pass
    def new_margin(self, margin, level): pass
    def new_spacing(self, spacing): pass
    def new_styles(self, styles): pass
    def send_paragraph(self, blankline): pass
    def send_line_break(self): pass
    def send_hor_rule(self, *args, **kw): pass
    def send_label_data(self, data): pass
    def send_flowing_data(self, data): pass
    def send_literal_data(self, data): pass

Guido van Rossum's avatar
Guido van Rossum committed
316

317
class AbstractWriter(NullWriter):
318 319 320 321 322 323
    """A writer which can be used in debugging formatters, but not much else.

    Each method simply announces itself by printing its name and
    arguments on standard output.

    """
Guido van Rossum's avatar
Guido van Rossum committed
324

325
    def new_alignment(self, align):
326
        print "new_alignment(%r)" % (align,)
327

Guido van Rossum's avatar
Guido van Rossum committed
328
    def new_font(self, font):
329
        print "new_font(%r)" % (font,)
Guido van Rossum's avatar
Guido van Rossum committed
330 331

    def new_margin(self, margin, level):
332
        print "new_margin(%r, %d)" % (margin, level)
Guido van Rossum's avatar
Guido van Rossum committed
333 334

    def new_spacing(self, spacing):
335
        print "new_spacing(%r)" % (spacing,)
Guido van Rossum's avatar
Guido van Rossum committed
336 337

    def new_styles(self, styles):
338
        print "new_styles(%r)" % (styles,)
Guido van Rossum's avatar
Guido van Rossum committed
339 340

    def send_paragraph(self, blankline):
341
        print "send_paragraph(%r)" % (blankline,)
Guido van Rossum's avatar
Guido van Rossum committed
342 343

    def send_line_break(self):
344
        print "send_line_break()"
Guido van Rossum's avatar
Guido van Rossum committed
345

346
    def send_hor_rule(self, *args, **kw):
347
        print "send_hor_rule()"
Guido van Rossum's avatar
Guido van Rossum committed
348 349

    def send_label_data(self, data):
350
        print "send_label_data(%r)" % (data,)
Guido van Rossum's avatar
Guido van Rossum committed
351 352

    def send_flowing_data(self, data):
353
        print "send_flowing_data(%r)" % (data,)
Guido van Rossum's avatar
Guido van Rossum committed
354 355

    def send_literal_data(self, data):
356
        print "send_literal_data(%r)" % (data,)
Guido van Rossum's avatar
Guido van Rossum committed
357 358


359
class DumbWriter(NullWriter):
360 361 362 363 364 365 366
    """Simple writer class which writes output on the file object passed in
    as the file parameter or, if file is omitted, on standard output.  The
    output is simply word-wrapped to the number of columns specified by
    the maxcol parameter.  This class is suitable for reflowing a sequence
    of paragraphs.

    """
Guido van Rossum's avatar
Guido van Rossum committed
367 368

    def __init__(self, file=None, maxcol=72):
369 370 371 372
        self.file = file or sys.stdout
        self.maxcol = maxcol
        NullWriter.__init__(self)
        self.reset()
Guido van Rossum's avatar
Guido van Rossum committed
373 374

    def reset(self):
375 376
        self.col = 0
        self.atbreak = 0
Guido van Rossum's avatar
Guido van Rossum committed
377 378

    def send_paragraph(self, blankline):
379
        self.file.write('\n'*blankline)
380 381
        self.col = 0
        self.atbreak = 0
Guido van Rossum's avatar
Guido van Rossum committed
382 383

    def send_line_break(self):
384 385 386
        self.file.write('\n')
        self.col = 0
        self.atbreak = 0
Guido van Rossum's avatar
Guido van Rossum committed
387

388
    def send_hor_rule(self, *args, **kw):
389 390 391 392 393
        self.file.write('\n')
        self.file.write('-'*self.maxcol)
        self.file.write('\n')
        self.col = 0
        self.atbreak = 0
Guido van Rossum's avatar
Guido van Rossum committed
394 395

    def send_literal_data(self, data):
396
        self.file.write(data)
397
        i = data.rfind('\n')
398 399 400
        if i >= 0:
            self.col = 0
            data = data[i+1:]
401
        data = data.expandtabs()
402 403
        self.col = self.col + len(data)
        self.atbreak = 0
Guido van Rossum's avatar
Guido van Rossum committed
404 405

    def send_flowing_data(self, data):
406
        if not data: return
407
        atbreak = self.atbreak or data[0].isspace()
408 409 410
        col = self.col
        maxcol = self.maxcol
        write = self.file.write
411
        for word in data.split():
412 413 414 415 416 417 418 419 420 421 422
            if atbreak:
                if col + len(word) >= maxcol:
                    write('\n')
                    col = 0
                else:
                    write(' ')
                    col = col + 1
            write(word)
            col = col + len(word)
            atbreak = 1
        self.col = col
423
        self.atbreak = data[-1].isspace()
Guido van Rossum's avatar
Guido van Rossum committed
424 425 426 427 428


def test(file = None):
    w = DumbWriter()
    f = AbstractFormatter(w)
429
    if file is not None:
430
        fp = open(file)
Guido van Rossum's avatar
Guido van Rossum committed
431
    elif sys.argv[1:]:
432
        fp = open(sys.argv[1])
Guido van Rossum's avatar
Guido van Rossum committed
433
    else:
434
        fp = sys.stdin
435
    for line in fp:
436 437 438 439
        if line == '\n':
            f.end_paragraph(1)
        else:
            f.add_flowing_data(line)
Guido van Rossum's avatar
Guido van Rossum committed
440 441 442 443 444
    f.end_paragraph(0)


if __name__ == '__main__':
    test()