formatter.py 14.6 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

231 232
    def push_font(self, font):
        size, i, b, tt = font
233 234 235 236 237 238 239 240 241 242 243 244 245
        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
246 247

    def pop_font(self):
248 249 250 251 252 253 254
        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
255 256

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

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

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

277
    def push_style(self, *styles):
278 279 280 281 282 283 284
        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
285

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

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


class NullWriter:
296 297 298 299 300 301 302
    """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.

    """
303
    def __init__(self): pass
304
    def flush(self): pass
305 306 307 308 309 310 311 312 313 314 315 316
    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
317

318
class AbstractWriter(NullWriter):
319 320 321 322 323 324
    """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
325

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

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

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

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

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

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

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

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

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

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

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


360
class DumbWriter(NullWriter):
361 362 363 364 365 366 367
    """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
368 369

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

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

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

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

389
    def send_hor_rule(self, *args, **kw):
390 391 392 393 394
        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
395 396

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

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


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


if __name__ == '__main__':
    test()