New \grammartoken markup, similar to \token but allowed everywhere.
[python/dscho.git] / Lib / formatter.py
blob0607526404ced4d19a05a7ec56068d91e29d3466
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 string
22 import sys
23 from types import StringType
26 AS_IS = None
29 class NullFormatter:
31 def __init__(self, writer=None):
32 if not writer:
33 writer = NullWriter()
34 self.writer = writer
35 def end_paragraph(self, blankline): pass
36 def add_line_break(self): pass
37 def add_hor_rule(self, *args, **kw): pass
38 def add_label_data(self, format, counter, blankline=None): pass
39 def add_flowing_data(self, data): pass
40 def add_literal_data(self, data): pass
41 def flush_softspace(self): pass
42 def push_alignment(self, align): pass
43 def pop_alignment(self): pass
44 def push_font(self, x): pass
45 def pop_font(self): pass
46 def push_margin(self, margin): pass
47 def pop_margin(self): pass
48 def set_spacing(self, spacing): pass
49 def push_style(self, *styles): pass
50 def pop_style(self, n=1): pass
51 def assert_line_data(self, flag=1): pass
54 class AbstractFormatter:
56 # Space handling policy: blank spaces at the boundary between elements
57 # are handled by the outermost context. "Literal" data is not checked
58 # to determine context, so spaces in literal data are handled directly
59 # in all circumstances.
61 def __init__(self, writer):
62 self.writer = writer # Output device
63 self.align = None # Current alignment
64 self.align_stack = [] # Alignment stack
65 self.font_stack = [] # Font state
66 self.margin_stack = [] # Margin state
67 self.spacing = None # Vertical spacing state
68 self.style_stack = [] # Other state, e.g. color
69 self.nospace = 1 # Should leading space be suppressed
70 self.softspace = 0 # Should a space be inserted
71 self.para_end = 1 # Just ended a paragraph
72 self.parskip = 0 # Skipped space between paragraphs?
73 self.hard_break = 1 # Have a hard break
74 self.have_label = 0
76 def end_paragraph(self, blankline):
77 if not self.hard_break:
78 self.writer.send_line_break()
79 self.have_label = 0
80 if self.parskip < blankline and not self.have_label:
81 self.writer.send_paragraph(blankline - self.parskip)
82 self.parskip = blankline
83 self.have_label = 0
84 self.hard_break = self.nospace = self.para_end = 1
85 self.softspace = 0
87 def add_line_break(self):
88 if not (self.hard_break or self.para_end):
89 self.writer.send_line_break()
90 self.have_label = self.parskip = 0
91 self.hard_break = self.nospace = 1
92 self.softspace = 0
94 def add_hor_rule(self, *args, **kw):
95 if not self.hard_break:
96 self.writer.send_line_break()
97 apply(self.writer.send_hor_rule, args, kw)
98 self.hard_break = self.nospace = 1
99 self.have_label = self.para_end = self.softspace = self.parskip = 0
101 def add_label_data(self, format, counter, blankline = None):
102 if self.have_label or not self.hard_break:
103 self.writer.send_line_break()
104 if not self.para_end:
105 self.writer.send_paragraph((blankline and 1) or 0)
106 if type(format) is StringType:
107 self.writer.send_label_data(self.format_counter(format, counter))
108 else:
109 self.writer.send_label_data(format)
110 self.nospace = self.have_label = self.hard_break = self.para_end = 1
111 self.softspace = self.parskip = 0
113 def format_counter(self, format, counter):
114 label = ''
115 for c in format:
116 if c == '1':
117 label = label + ('%d' % counter)
118 elif c in 'aA':
119 if counter > 0:
120 label = label + self.format_letter(c, counter)
121 elif c in 'iI':
122 if counter > 0:
123 label = label + self.format_roman(c, counter)
124 else:
125 label = label + c
126 return label
128 def format_letter(self, case, counter):
129 label = ''
130 while counter > 0:
131 counter, x = divmod(counter-1, 26)
132 # This makes a strong assumption that lowercase letters
133 # and uppercase letters form two contiguous blocks, with
134 # letters in order!
135 s = chr(ord(case) + x)
136 label = s + label
137 return label
139 def format_roman(self, case, counter):
140 ones = ['i', 'x', 'c', 'm']
141 fives = ['v', 'l', 'd']
142 label, index = '', 0
143 # This will die of IndexError when counter is too big
144 while counter > 0:
145 counter, x = divmod(counter, 10)
146 if x == 9:
147 label = ones[index] + ones[index+1] + label
148 elif x == 4:
149 label = ones[index] + fives[index] + label
150 else:
151 if x >= 5:
152 s = fives[index]
153 x = x-5
154 else:
155 s = ''
156 s = s + ones[index]*x
157 label = s + label
158 index = index + 1
159 if case == 'I':
160 return label.upper()
161 return label
163 def add_flowing_data(self, data,
164 # These are only here to load them into locals:
165 whitespace = string.whitespace,
166 join = string.join, split = string.split):
167 if not data: return
168 # The following looks a bit convoluted but is a great improvement over
169 # data = regsub.gsub('[' + string.whitespace + ']+', ' ', data)
170 prespace = data[:1] in whitespace
171 postspace = data[-1:] in whitespace
172 data = join(split(data))
173 if self.nospace and not data:
174 return
175 elif prespace or self.softspace:
176 if not data:
177 if not self.nospace:
178 self.softspace = 1
179 self.parskip = 0
180 return
181 if not self.nospace:
182 data = ' ' + data
183 self.hard_break = self.nospace = self.para_end = \
184 self.parskip = self.have_label = 0
185 self.softspace = postspace
186 self.writer.send_flowing_data(data)
188 def add_literal_data(self, data):
189 if not data: return
190 if self.softspace:
191 self.writer.send_flowing_data(" ")
192 self.hard_break = data[-1:] == '\n'
193 self.nospace = self.para_end = self.softspace = \
194 self.parskip = self.have_label = 0
195 self.writer.send_literal_data(data)
197 def flush_softspace(self):
198 if self.softspace:
199 self.hard_break = self.para_end = self.parskip = \
200 self.have_label = self.softspace = 0
201 self.nospace = 1
202 self.writer.send_flowing_data(' ')
204 def push_alignment(self, align):
205 if align and align != self.align:
206 self.writer.new_alignment(align)
207 self.align = align
208 self.align_stack.append(align)
209 else:
210 self.align_stack.append(self.align)
212 def pop_alignment(self):
213 if self.align_stack:
214 del self.align_stack[-1]
215 if self.align_stack:
216 self.align = align = self.align_stack[-1]
217 self.writer.new_alignment(align)
218 else:
219 self.align = None
220 self.writer.new_alignment(None)
222 def push_font(self, (size, i, b, tt)):
223 if self.softspace:
224 self.hard_break = self.para_end = self.softspace = 0
225 self.nospace = 1
226 self.writer.send_flowing_data(' ')
227 if self.font_stack:
228 csize, ci, cb, ctt = self.font_stack[-1]
229 if size is AS_IS: size = csize
230 if i is AS_IS: i = ci
231 if b is AS_IS: b = cb
232 if tt is AS_IS: tt = ctt
233 font = (size, i, b, tt)
234 self.font_stack.append(font)
235 self.writer.new_font(font)
237 def pop_font(self):
238 if self.font_stack:
239 del self.font_stack[-1]
240 if self.font_stack:
241 font = self.font_stack[-1]
242 else:
243 font = None
244 self.writer.new_font(font)
246 def push_margin(self, margin):
247 self.margin_stack.append(margin)
248 fstack = filter(None, self.margin_stack)
249 if not margin and fstack:
250 margin = fstack[-1]
251 self.writer.new_margin(margin, len(fstack))
253 def pop_margin(self):
254 if self.margin_stack:
255 del self.margin_stack[-1]
256 fstack = filter(None, self.margin_stack)
257 if fstack:
258 margin = fstack[-1]
259 else:
260 margin = None
261 self.writer.new_margin(margin, len(fstack))
263 def set_spacing(self, spacing):
264 self.spacing = spacing
265 self.writer.new_spacing(spacing)
267 def push_style(self, *styles):
268 if self.softspace:
269 self.hard_break = self.para_end = self.softspace = 0
270 self.nospace = 1
271 self.writer.send_flowing_data(' ')
272 for style in styles:
273 self.style_stack.append(style)
274 self.writer.new_styles(tuple(self.style_stack))
276 def pop_style(self, n=1):
277 del self.style_stack[-n:]
278 self.writer.new_styles(tuple(self.style_stack))
280 def assert_line_data(self, flag=1):
281 self.nospace = self.hard_break = not flag
282 self.para_end = self.parskip = self.have_label = 0
285 class NullWriter:
286 """Minimal writer interface to use in testing & inheritance."""
287 def __init__(self): pass
288 def flush(self): pass
289 def new_alignment(self, align): pass
290 def new_font(self, font): pass
291 def new_margin(self, margin, level): pass
292 def new_spacing(self, spacing): pass
293 def new_styles(self, styles): pass
294 def send_paragraph(self, blankline): pass
295 def send_line_break(self): pass
296 def send_hor_rule(self, *args, **kw): pass
297 def send_label_data(self, data): pass
298 def send_flowing_data(self, data): pass
299 def send_literal_data(self, data): pass
302 class AbstractWriter(NullWriter):
304 def new_alignment(self, align):
305 print "new_alignment(%s)" % `align`
307 def new_font(self, font):
308 print "new_font(%s)" % `font`
310 def new_margin(self, margin, level):
311 print "new_margin(%s, %d)" % (`margin`, level)
313 def new_spacing(self, spacing):
314 print "new_spacing(%s)" % `spacing`
316 def new_styles(self, styles):
317 print "new_styles(%s)" % `styles`
319 def send_paragraph(self, blankline):
320 print "send_paragraph(%s)" % `blankline`
322 def send_line_break(self):
323 print "send_line_break()"
325 def send_hor_rule(self, *args, **kw):
326 print "send_hor_rule()"
328 def send_label_data(self, data):
329 print "send_label_data(%s)" % `data`
331 def send_flowing_data(self, data):
332 print "send_flowing_data(%s)" % `data`
334 def send_literal_data(self, data):
335 print "send_literal_data(%s)" % `data`
338 class DumbWriter(NullWriter):
340 def __init__(self, file=None, maxcol=72):
341 self.file = file or sys.stdout
342 self.maxcol = maxcol
343 NullWriter.__init__(self)
344 self.reset()
346 def reset(self):
347 self.col = 0
348 self.atbreak = 0
350 def send_paragraph(self, blankline):
351 self.file.write('\n'*blankline)
352 self.col = 0
353 self.atbreak = 0
355 def send_line_break(self):
356 self.file.write('\n')
357 self.col = 0
358 self.atbreak = 0
360 def send_hor_rule(self, *args, **kw):
361 self.file.write('\n')
362 self.file.write('-'*self.maxcol)
363 self.file.write('\n')
364 self.col = 0
365 self.atbreak = 0
367 def send_literal_data(self, data):
368 self.file.write(data)
369 i = data.rfind('\n')
370 if i >= 0:
371 self.col = 0
372 data = data[i+1:]
373 data = data.expandtabs()
374 self.col = self.col + len(data)
375 self.atbreak = 0
377 def send_flowing_data(self, data):
378 if not data: return
379 atbreak = self.atbreak or data[0] in string.whitespace
380 col = self.col
381 maxcol = self.maxcol
382 write = self.file.write
383 for word in data.split():
384 if atbreak:
385 if col + len(word) >= maxcol:
386 write('\n')
387 col = 0
388 else:
389 write(' ')
390 col = col + 1
391 write(word)
392 col = col + len(word)
393 atbreak = 1
394 self.col = col
395 self.atbreak = data[-1] in string.whitespace
398 def test(file = None):
399 w = DumbWriter()
400 f = AbstractFormatter(w)
401 if file:
402 fp = open(file)
403 elif sys.argv[1:]:
404 fp = open(sys.argv[1])
405 else:
406 fp = sys.stdin
407 while 1:
408 line = fp.readline()
409 if not line:
410 break
411 if line == '\n':
412 f.end_paragraph(1)
413 else:
414 f.add_flowing_data(line)
415 f.end_paragraph(0)
418 if __name__ == '__main__':
419 test()