formatter.py 14.7 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 111 112 113
        if not self.hard_break:
            self.writer.send_line_break()
        apply(self.writer.send_hor_rule, args, kw)
        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 179 180
        if not data: return
        # The following looks a bit convoluted but is a great improvement over
        # data = regsub.gsub('[' + string.whitespace + ']+', ' ', data)
181 182 183
        prespace = data[:1].isspace()
        postspace = data[-1:].isspace()
        data = " ".join(data.split())
184 185 186 187 188 189 190 191 192 193 194 195 196 197
        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
198 199

    def add_literal_data(self, data):
200 201 202 203 204 205 206
        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
207

208
    def flush_softspace(self):
209 210 211 212 213
        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(' ')
214

215
    def push_alignment(self, align):
216 217 218 219 220 221
        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)
222 223

    def pop_alignment(self):
224 225 226 227 228 229 230 231
        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)
232

Guido van Rossum's avatar
Guido van Rossum committed
233
    def push_font(self, (size, i, b, tt)):
234 235 236 237 238 239 240 241 242 243 244 245 246
        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
247 248

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

    def push_margin(self, margin):
258 259 260 261 262
        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
263 264

    def pop_margin(self):
265 266 267 268 269 270 271 272
        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
273 274

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

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

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

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


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

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

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

327
    def new_alignment(self, align):
328
        print "new_alignment(%s)" % `align`
329

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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


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


if __name__ == '__main__':
    test()