Misc. changes, including documenting the ability to specify a class attribute in...
[python.git] / Lib / formatter.py
blobfa2b38938ae6e9b228be32dad2aeb903516d920a
1 """Generic output formatting.
3 Formatter objects transform an abstract flow of formatting events into
4 specific output events on writer objects. Formatters manage several stack
5 structures to allow various properties of a writer object to be changed and
6 restored; writers need not be able to handle relative changes nor any sort
7 of ``change back'' operation. Specific writer properties which may be
8 controlled via formatter objects are horizontal alignment, font, and left
9 margin indentations. A mechanism is provided which supports providing
10 arbitrary, non-exclusive style settings to a writer as well. Additional
11 interfaces facilitate formatting events which are not reversible, such as
12 paragraph separation.
14 Writer objects encapsulate device interfaces. Abstract devices, such as
15 file formats, are supported as well as physical devices. The provided
16 implementations all work with abstract devices. The interface makes
17 available mechanisms for setting the properties which formatter objects
18 manage and inserting data into the output.
19 """
21 import sys
24 AS_IS = None
27 class NullFormatter:
28 """A formatter which does nothing.
30 If the writer parameter is omitted, a NullWriter instance is created.
31 No methods of the writer are called by NullFormatter instances.
33 Implementations should inherit from this class if implementing a writer
34 interface but don't need to inherit any implementation.
36 """
38 def __init__(self, writer=None):
39 if writer is None:
40 writer = NullWriter()
41 self.writer = writer
42 def end_paragraph(self, blankline): pass
43 def add_line_break(self): pass
44 def add_hor_rule(self, *args, **kw): pass
45 def add_label_data(self, format, counter, blankline=None): pass
46 def add_flowing_data(self, data): pass
47 def add_literal_data(self, data): pass
48 def flush_softspace(self): pass
49 def push_alignment(self, align): pass
50 def pop_alignment(self): pass
51 def push_font(self, x): pass
52 def pop_font(self): pass
53 def push_margin(self, margin): pass
54 def pop_margin(self): pass
55 def set_spacing(self, spacing): pass
56 def push_style(self, *styles): pass
57 def pop_style(self, n=1): pass
58 def assert_line_data(self, flag=1): pass
61 class AbstractFormatter:
62 """The standard formatter.
64 This implementation has demonstrated wide applicability to many writers,
65 and may be used directly in most circumstances. It has been used to
66 implement a full-featured World Wide Web browser.
68 """
70 # Space handling policy: blank spaces at the boundary between elements
71 # are handled by the outermost context. "Literal" data is not checked
72 # to determine context, so spaces in literal data are handled directly
73 # in all circumstances.
75 def __init__(self, writer):
76 self.writer = writer # Output device
77 self.align = None # Current alignment
78 self.align_stack = [] # Alignment stack
79 self.font_stack = [] # Font state
80 self.margin_stack = [] # Margin state
81 self.spacing = None # Vertical spacing state
82 self.style_stack = [] # Other state, e.g. color
83 self.nospace = 1 # Should leading space be suppressed
84 self.softspace = 0 # Should a space be inserted
85 self.para_end = 1 # Just ended a paragraph
86 self.parskip = 0 # Skipped space between paragraphs?
87 self.hard_break = 1 # Have a hard break
88 self.have_label = 0
90 def end_paragraph(self, blankline):
91 if not self.hard_break:
92 self.writer.send_line_break()
93 self.have_label = 0
94 if self.parskip < blankline and not self.have_label:
95 self.writer.send_paragraph(blankline - self.parskip)
96 self.parskip = blankline
97 self.have_label = 0
98 self.hard_break = self.nospace = self.para_end = 1
99 self.softspace = 0
101 def add_line_break(self):
102 if not (self.hard_break or self.para_end):
103 self.writer.send_line_break()
104 self.have_label = self.parskip = 0
105 self.hard_break = self.nospace = 1
106 self.softspace = 0
108 def add_hor_rule(self, *args, **kw):
109 if not self.hard_break:
110 self.writer.send_line_break()
111 self.writer.send_hor_rule(*args, **kw)
112 self.hard_break = self.nospace = 1
113 self.have_label = self.para_end = self.softspace = self.parskip = 0
115 def add_label_data(self, format, counter, blankline = None):
116 if self.have_label or not self.hard_break:
117 self.writer.send_line_break()
118 if not self.para_end:
119 self.writer.send_paragraph((blankline and 1) or 0)
120 if isinstance(format, str):
121 self.writer.send_label_data(self.format_counter(format, counter))
122 else:
123 self.writer.send_label_data(format)
124 self.nospace = self.have_label = self.hard_break = self.para_end = 1
125 self.softspace = self.parskip = 0
127 def format_counter(self, format, counter):
128 label = ''
129 for c in format:
130 if c == '1':
131 label = label + ('%d' % counter)
132 elif c in 'aA':
133 if counter > 0:
134 label = label + self.format_letter(c, counter)
135 elif c in 'iI':
136 if counter > 0:
137 label = label + self.format_roman(c, counter)
138 else:
139 label = label + c
140 return label
142 def format_letter(self, case, counter):
143 label = ''
144 while counter > 0:
145 counter, x = divmod(counter-1, 26)
146 # This makes a strong assumption that lowercase letters
147 # and uppercase letters form two contiguous blocks, with
148 # letters in order!
149 s = chr(ord(case) + x)
150 label = s + label
151 return label
153 def format_roman(self, case, counter):
154 ones = ['i', 'x', 'c', 'm']
155 fives = ['v', 'l', 'd']
156 label, index = '', 0
157 # This will die of IndexError when counter is too big
158 while counter > 0:
159 counter, x = divmod(counter, 10)
160 if x == 9:
161 label = ones[index] + ones[index+1] + label
162 elif x == 4:
163 label = ones[index] + fives[index] + label
164 else:
165 if x >= 5:
166 s = fives[index]
167 x = x-5
168 else:
169 s = ''
170 s = s + ones[index]*x
171 label = s + label
172 index = index + 1
173 if case == 'I':
174 return label.upper()
175 return label
177 def add_flowing_data(self, data):
178 if not data: return
179 prespace = data[:1].isspace()
180 postspace = data[-1:].isspace()
181 data = " ".join(data.split())
182 if self.nospace and not data:
183 return
184 elif prespace or self.softspace:
185 if not data:
186 if not self.nospace:
187 self.softspace = 1
188 self.parskip = 0
189 return
190 if not self.nospace:
191 data = ' ' + data
192 self.hard_break = self.nospace = self.para_end = \
193 self.parskip = self.have_label = 0
194 self.softspace = postspace
195 self.writer.send_flowing_data(data)
197 def add_literal_data(self, data):
198 if not data: return
199 if self.softspace:
200 self.writer.send_flowing_data(" ")
201 self.hard_break = data[-1:] == '\n'
202 self.nospace = self.para_end = self.softspace = \
203 self.parskip = self.have_label = 0
204 self.writer.send_literal_data(data)
206 def flush_softspace(self):
207 if self.softspace:
208 self.hard_break = self.para_end = self.parskip = \
209 self.have_label = self.softspace = 0
210 self.nospace = 1
211 self.writer.send_flowing_data(' ')
213 def push_alignment(self, align):
214 if align and align != self.align:
215 self.writer.new_alignment(align)
216 self.align = align
217 self.align_stack.append(align)
218 else:
219 self.align_stack.append(self.align)
221 def pop_alignment(self):
222 if self.align_stack:
223 del self.align_stack[-1]
224 if self.align_stack:
225 self.align = align = self.align_stack[-1]
226 self.writer.new_alignment(align)
227 else:
228 self.align = None
229 self.writer.new_alignment(None)
231 def push_font(self, (size, i, b, tt)):
232 if self.softspace:
233 self.hard_break = self.para_end = self.softspace = 0
234 self.nospace = 1
235 self.writer.send_flowing_data(' ')
236 if self.font_stack:
237 csize, ci, cb, ctt = self.font_stack[-1]
238 if size is AS_IS: size = csize
239 if i is AS_IS: i = ci
240 if b is AS_IS: b = cb
241 if tt is AS_IS: tt = ctt
242 font = (size, i, b, tt)
243 self.font_stack.append(font)
244 self.writer.new_font(font)
246 def pop_font(self):
247 if self.font_stack:
248 del self.font_stack[-1]
249 if self.font_stack:
250 font = self.font_stack[-1]
251 else:
252 font = None
253 self.writer.new_font(font)
255 def push_margin(self, margin):
256 self.margin_stack.append(margin)
257 fstack = filter(None, self.margin_stack)
258 if not margin and fstack:
259 margin = fstack[-1]
260 self.writer.new_margin(margin, len(fstack))
262 def pop_margin(self):
263 if self.margin_stack:
264 del self.margin_stack[-1]
265 fstack = filter(None, self.margin_stack)
266 if fstack:
267 margin = fstack[-1]
268 else:
269 margin = None
270 self.writer.new_margin(margin, len(fstack))
272 def set_spacing(self, spacing):
273 self.spacing = spacing
274 self.writer.new_spacing(spacing)
276 def push_style(self, *styles):
277 if self.softspace:
278 self.hard_break = self.para_end = self.softspace = 0
279 self.nospace = 1
280 self.writer.send_flowing_data(' ')
281 for style in styles:
282 self.style_stack.append(style)
283 self.writer.new_styles(tuple(self.style_stack))
285 def pop_style(self, n=1):
286 del self.style_stack[-n:]
287 self.writer.new_styles(tuple(self.style_stack))
289 def assert_line_data(self, flag=1):
290 self.nospace = self.hard_break = not flag
291 self.para_end = self.parskip = self.have_label = 0
294 class NullWriter:
295 """Minimal writer interface to use in testing & inheritance.
297 A writer which only provides the interface definition; no actions are
298 taken on any methods. This should be the base class for all writers
299 which do not need to inherit any implementation methods.
302 def __init__(self): pass
303 def flush(self): pass
304 def new_alignment(self, align): pass
305 def new_font(self, font): pass
306 def new_margin(self, margin, level): pass
307 def new_spacing(self, spacing): pass
308 def new_styles(self, styles): pass
309 def send_paragraph(self, blankline): pass
310 def send_line_break(self): pass
311 def send_hor_rule(self, *args, **kw): pass
312 def send_label_data(self, data): pass
313 def send_flowing_data(self, data): pass
314 def send_literal_data(self, data): pass
317 class AbstractWriter(NullWriter):
318 """A writer which can be used in debugging formatters, but not much else.
320 Each method simply announces itself by printing its name and
321 arguments on standard output.
325 def new_alignment(self, align):
326 print "new_alignment(%r)" % (align,)
328 def new_font(self, font):
329 print "new_font(%r)" % (font,)
331 def new_margin(self, margin, level):
332 print "new_margin(%r, %d)" % (margin, level)
334 def new_spacing(self, spacing):
335 print "new_spacing(%r)" % (spacing,)
337 def new_styles(self, styles):
338 print "new_styles(%r)" % (styles,)
340 def send_paragraph(self, blankline):
341 print "send_paragraph(%r)" % (blankline,)
343 def send_line_break(self):
344 print "send_line_break()"
346 def send_hor_rule(self, *args, **kw):
347 print "send_hor_rule()"
349 def send_label_data(self, data):
350 print "send_label_data(%r)" % (data,)
352 def send_flowing_data(self, data):
353 print "send_flowing_data(%r)" % (data,)
355 def send_literal_data(self, data):
356 print "send_literal_data(%r)" % (data,)
359 class DumbWriter(NullWriter):
360 """Simple writer class which writes output on the file object passed in
361 as the file parameter or, if file is omitted, on standard output. The
362 output is simply word-wrapped to the number of columns specified by
363 the maxcol parameter. This class is suitable for reflowing a sequence
364 of paragraphs.
368 def __init__(self, file=None, maxcol=72):
369 self.file = file or sys.stdout
370 self.maxcol = maxcol
371 NullWriter.__init__(self)
372 self.reset()
374 def reset(self):
375 self.col = 0
376 self.atbreak = 0
378 def send_paragraph(self, blankline):
379 self.file.write('\n'*blankline)
380 self.col = 0
381 self.atbreak = 0
383 def send_line_break(self):
384 self.file.write('\n')
385 self.col = 0
386 self.atbreak = 0
388 def send_hor_rule(self, *args, **kw):
389 self.file.write('\n')
390 self.file.write('-'*self.maxcol)
391 self.file.write('\n')
392 self.col = 0
393 self.atbreak = 0
395 def send_literal_data(self, data):
396 self.file.write(data)
397 i = data.rfind('\n')
398 if i >= 0:
399 self.col = 0
400 data = data[i+1:]
401 data = data.expandtabs()
402 self.col = self.col + len(data)
403 self.atbreak = 0
405 def send_flowing_data(self, data):
406 if not data: return
407 atbreak = self.atbreak or data[0].isspace()
408 col = self.col
409 maxcol = self.maxcol
410 write = self.file.write
411 for word in data.split():
412 if atbreak:
413 if col + len(word) >= maxcol:
414 write('\n')
415 col = 0
416 else:
417 write(' ')
418 col = col + 1
419 write(word)
420 col = col + len(word)
421 atbreak = 1
422 self.col = col
423 self.atbreak = data[-1].isspace()
426 def test(file = None):
427 w = DumbWriter()
428 f = AbstractFormatter(w)
429 if file is not None:
430 fp = open(file)
431 elif sys.argv[1:]:
432 fp = open(sys.argv[1])
433 else:
434 fp = sys.stdin
435 while 1:
436 line = fp.readline()
437 if not line:
438 break
439 if line == '\n':
440 f.end_paragraph(1)
441 else:
442 f.add_flowing_data(line)
443 f.end_paragraph(0)
446 if __name__ == '__main__':
447 test()