formatter.py 14.8 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
import sys
22
import warnings
23 24
warnings.warn('the formatter module is deprecated', DeprecationWarning,
              stacklevel=2)
Guido van Rossum's avatar
Guido van Rossum committed
25 26 27 28 29


AS_IS = None


Guido van Rossum's avatar
Guido van Rossum committed
30
class NullFormatter:
31 32 33 34 35 36 37 38 39
    """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
40

41
    def __init__(self, writer=None):
42
        if writer is None:
43 44
            writer = NullWriter()
        self.writer = writer
Guido van Rossum's avatar
Guido van Rossum committed
45 46
    def end_paragraph(self, blankline): pass
    def add_line_break(self): pass
47 48
    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
49 50 51
    def add_flowing_data(self, data): pass
    def add_literal_data(self, data): pass
    def flush_softspace(self): pass
52 53
    def push_alignment(self, align): pass
    def pop_alignment(self): pass
Guido van Rossum's avatar
Guido van Rossum committed
54 55 56 57 58
    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
59 60 61
    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
62 63


Guido van Rossum's avatar
Guido van Rossum committed
64
class AbstractFormatter:
65 66 67 68 69 70 71
    """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
72

73 74 75 76 77
    #  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
78
    def __init__(self, writer):
79 80 81 82 83 84 85 86 87 88 89 90 91
        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
92 93

    def end_paragraph(self, blankline):
94 95 96 97 98 99 100 101 102
        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
103 104

    def add_line_break(self):
105 106 107 108 109
        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
110

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

    def add_label_data(self, format, counter, blankline = None):
119 120 121 122
        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)
123
        if isinstance(format, str):
124 125 126 127 128
            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
129 130 131 132

    def format_counter(self, format, counter):
        label = ''
        for c in format:
133 134 135 136 137 138 139 140 141
            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:
142
                label = label + c
Guido van Rossum's avatar
Guido van Rossum committed
143 144 145
        return label

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

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

180
    def add_flowing_data(self, data):
181
        if not data: return
182 183 184
        prespace = data[:1].isspace()
        postspace = data[-1:].isspace()
        data = " ".join(data.split())
185 186 187 188 189 190 191 192 193 194 195 196 197 198
        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
199 200

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

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

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

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

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

    def pop_font(self):
251 252 253 254 255 256 257
        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
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 263 264
        if not margin and fstack:
            margin = fstack[-1]
        self.writer.new_margin(margin, len(fstack))
Guido van Rossum's avatar
Guido van Rossum committed
265 266

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

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

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

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

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


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

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

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

329
    def new_alignment(self, align):
330
        print("new_alignment(%r)" % (align,))
331

Guido van Rossum's avatar
Guido van Rossum committed
332
    def new_font(self, font):
333
        print("new_font(%r)" % (font,))
Guido van Rossum's avatar
Guido van Rossum committed
334 335

    def new_margin(self, margin, level):
336
        print("new_margin(%r, %d)" % (margin, level))
Guido van Rossum's avatar
Guido van Rossum committed
337 338

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

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

    def send_paragraph(self, blankline):
345
        print("send_paragraph(%r)" % (blankline,))
Guido van Rossum's avatar
Guido van Rossum committed
346 347

    def send_line_break(self):
348
        print("send_line_break()")
Guido van Rossum's avatar
Guido van Rossum committed
349

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

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

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

    def send_literal_data(self, data):
360
        print("send_literal_data(%r)" % (data,))
Guido van Rossum's avatar
Guido van Rossum committed
361 362


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

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

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

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

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

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

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

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


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


if __name__ == '__main__':
    test()