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