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
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.
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.
38 def __init__(self
, writer
=None):
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.
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
90 def end_paragraph(self
, blankline
):
91 if not self
.hard_break
:
92 self
.writer
.send_line_break()
94 if self
.parskip
< blankline
and not self
.have_label
:
95 self
.writer
.send_paragraph(blankline
- self
.parskip
)
96 self
.parskip
= blankline
98 self
.hard_break
= self
.nospace
= self
.para_end
= 1
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
108 def add_hor_rule(self
, *args
, **kw
):
109 if not self
.hard_break
:
110 self
.writer
.send_line_break()
111 apply(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
))
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
):
131 label
= label
+ ('%d' % counter
)
134 label
= label
+ self
.format_letter(c
, counter
)
137 label
= label
+ self
.format_roman(c
, counter
)
142 def format_letter(self
, case
, counter
):
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
149 s
= chr(ord(case
) + x
)
153 def format_roman(self
, case
, counter
):
154 ones
= ['i', 'x', 'c', 'm']
155 fives
= ['v', 'l', 'd']
157 # This will die of IndexError when counter is too big
159 counter
, x
= divmod(counter
, 10)
161 label
= ones
[index
] + ones
[index
+1] + label
163 label
= ones
[index
] + fives
[index
] + label
170 s
= s
+ ones
[index
]*x
177 def add_flowing_data(self
, data
):
179 # The following looks a bit convoluted but is a great improvement over
180 # data = regsub.gsub('[' + string.whitespace + ']+', ' ', data)
181 prespace
= data
[:1].isspace()
182 postspace
= data
[-1:].isspace()
183 data
= " ".join(data
.split())
184 if self
.nospace
and not data
:
186 elif prespace
or self
.softspace
:
194 self
.hard_break
= self
.nospace
= self
.para_end
= \
195 self
.parskip
= self
.have_label
= 0
196 self
.softspace
= postspace
197 self
.writer
.send_flowing_data(data
)
199 def add_literal_data(self
, data
):
202 self
.writer
.send_flowing_data(" ")
203 self
.hard_break
= data
[-1:] == '\n'
204 self
.nospace
= self
.para_end
= self
.softspace
= \
205 self
.parskip
= self
.have_label
= 0
206 self
.writer
.send_literal_data(data
)
208 def flush_softspace(self
):
210 self
.hard_break
= self
.para_end
= self
.parskip
= \
211 self
.have_label
= self
.softspace
= 0
213 self
.writer
.send_flowing_data(' ')
215 def push_alignment(self
, align
):
216 if align
and align
!= self
.align
:
217 self
.writer
.new_alignment(align
)
219 self
.align_stack
.append(align
)
221 self
.align_stack
.append(self
.align
)
223 def pop_alignment(self
):
225 del self
.align_stack
[-1]
227 self
.align
= align
= self
.align_stack
[-1]
228 self
.writer
.new_alignment(align
)
231 self
.writer
.new_alignment(None)
233 def push_font(self
, (size
, i
, b
, tt
)):
235 self
.hard_break
= self
.para_end
= self
.softspace
= 0
237 self
.writer
.send_flowing_data(' ')
239 csize
, ci
, cb
, ctt
= self
.font_stack
[-1]
240 if size
is AS_IS
: size
= csize
241 if i
is AS_IS
: i
= ci
242 if b
is AS_IS
: b
= cb
243 if tt
is AS_IS
: tt
= ctt
244 font
= (size
, i
, b
, tt
)
245 self
.font_stack
.append(font
)
246 self
.writer
.new_font(font
)
250 del self
.font_stack
[-1]
252 font
= self
.font_stack
[-1]
255 self
.writer
.new_font(font
)
257 def push_margin(self
, margin
):
258 self
.margin_stack
.append(margin
)
259 fstack
= filter(None, self
.margin_stack
)
260 if not margin
and fstack
:
262 self
.writer
.new_margin(margin
, len(fstack
))
264 def pop_margin(self
):
265 if self
.margin_stack
:
266 del self
.margin_stack
[-1]
267 fstack
= filter(None, self
.margin_stack
)
272 self
.writer
.new_margin(margin
, len(fstack
))
274 def set_spacing(self
, spacing
):
275 self
.spacing
= spacing
276 self
.writer
.new_spacing(spacing
)
278 def push_style(self
, *styles
):
280 self
.hard_break
= self
.para_end
= self
.softspace
= 0
282 self
.writer
.send_flowing_data(' ')
284 self
.style_stack
.append(style
)
285 self
.writer
.new_styles(tuple(self
.style_stack
))
287 def pop_style(self
, n
=1):
288 del self
.style_stack
[-n
:]
289 self
.writer
.new_styles(tuple(self
.style_stack
))
291 def assert_line_data(self
, flag
=1):
292 self
.nospace
= self
.hard_break
= not flag
293 self
.para_end
= self
.parskip
= self
.have_label
= 0
297 """Minimal writer interface to use in testing & inheritance.
299 A writer which only provides the interface definition; no actions are
300 taken on any methods. This should be the base class for all writers
301 which do not need to inherit any implementation methods.
304 def __init__(self
): pass
305 def flush(self
): pass
306 def new_alignment(self
, align
): pass
307 def new_font(self
, font
): pass
308 def new_margin(self
, margin
, level
): pass
309 def new_spacing(self
, spacing
): pass
310 def new_styles(self
, styles
): pass
311 def send_paragraph(self
, blankline
): pass
312 def send_line_break(self
): pass
313 def send_hor_rule(self
, *args
, **kw
): pass
314 def send_label_data(self
, data
): pass
315 def send_flowing_data(self
, data
): pass
316 def send_literal_data(self
, data
): pass
319 class AbstractWriter(NullWriter
):
320 """A writer which can be used in debugging formatters, but not much else.
322 Each method simply announces itself by printing its name and
323 arguments on standard output.
327 def new_alignment(self
, align
):
328 print "new_alignment(%s)" % `align`
330 def new_font(self
, font
):
331 print "new_font(%s)" % `font`
333 def new_margin(self
, margin
, level
):
334 print "new_margin(%s, %d)" % (`margin`
, level
)
336 def new_spacing(self
, spacing
):
337 print "new_spacing(%s)" % `spacing`
339 def new_styles(self
, styles
):
340 print "new_styles(%s)" % `styles`
342 def send_paragraph(self
, blankline
):
343 print "send_paragraph(%s)" % `blankline`
345 def send_line_break(self
):
346 print "send_line_break()"
348 def send_hor_rule(self
, *args
, **kw
):
349 print "send_hor_rule()"
351 def send_label_data(self
, data
):
352 print "send_label_data(%s)" % `data`
354 def send_flowing_data(self
, data
):
355 print "send_flowing_data(%s)" % `data`
357 def send_literal_data(self
, data
):
358 print "send_literal_data(%s)" % `data`
361 class DumbWriter(NullWriter
):
362 """Simple writer class which writes output on the file object passed in
363 as the file parameter or, if file is omitted, on standard output. The
364 output is simply word-wrapped to the number of columns specified by
365 the maxcol parameter. This class is suitable for reflowing a sequence
370 def __init__(self
, file=None, maxcol
=72):
371 self
.file = file or sys
.stdout
373 NullWriter
.__init
__(self
)
380 def send_paragraph(self
, blankline
):
381 self
.file.write('\n'*blankline
)
385 def send_line_break(self
):
386 self
.file.write('\n')
390 def send_hor_rule(self
, *args
, **kw
):
391 self
.file.write('\n')
392 self
.file.write('-'*self
.maxcol
)
393 self
.file.write('\n')
397 def send_literal_data(self
, data
):
398 self
.file.write(data
)
403 data
= data
.expandtabs()
404 self
.col
= self
.col
+ len(data
)
407 def send_flowing_data(self
, data
):
409 atbreak
= self
.atbreak
or data
[0].isspace()
412 write
= self
.file.write
413 for word
in data
.split():
415 if col
+ len(word
) >= maxcol
:
422 col
= col
+ len(word
)
425 self
.atbreak
= data
[-1].isspace()
428 def test(file = None):
430 f
= AbstractFormatter(w
)
434 fp
= open(sys
.argv
[1])
444 f
.add_flowing_data(line
)
448 if __name__
== '__main__':