Skip to content
This repository
Newer
Older
100644 377 lines (342 sloc) 9.086 kb
511dc44a » Laurent Sansonetti
2008-02-25 initial import
1 # This class implements a pretty printing algorithm. It finds line breaks and
2 # nice indentations for grouped structure.
8f211620 » richkilmer
2009-03-02 bring lib up to r22701 (ruby 1.9.1_0 tag). there are build issues usi…
3 #
511dc44a » Laurent Sansonetti
2008-02-25 initial import
4 # By default, the class assumes that primitive elements are strings and each
5 # byte in the strings have single column in width. But it can be used for
6 # other situations by giving suitable arguments for some methods:
7 # * newline object and space generation block for PrettyPrint.new
8 # * optional width argument for PrettyPrint#text
9 # * PrettyPrint#breakable
10 #
11 # There are several candidate uses:
12 # * text formatting using proportional fonts
13 # * multibyte characters which has columns different to number of bytes
14 # * non-string formatting
15 #
16 # == Bugs
17 # * Box based formatting?
18 # * Other (better) model/algorithm?
8f211620 » richkilmer
2009-03-02 bring lib up to r22701 (ruby 1.9.1_0 tag). there are build issues usi…
19 #
511dc44a » Laurent Sansonetti
2008-02-25 initial import
20 # == References
21 # Christian Lindig, Strictly Pretty, March 2000,
22 # http://www.st.cs.uni-sb.de/~lindig/papers/#pretty
8f211620 » richkilmer
2009-03-02 bring lib up to r22701 (ruby 1.9.1_0 tag). there are build issues usi…
23 #
511dc44a » Laurent Sansonetti
2008-02-25 initial import
24 # Philip Wadler, A prettier printer, March 1998,
25 # http://homepages.inf.ed.ac.uk/wadler/topics/language-design.html#prettier
8f211620 » richkilmer
2009-03-02 bring lib up to r22701 (ruby 1.9.1_0 tag). there are build issues usi…
26 #
511dc44a » Laurent Sansonetti
2008-02-25 initial import
27 # == Author
28 # Tanaka Akira <akr@m17n.org>
8f211620 » richkilmer
2009-03-02 bring lib up to r22701 (ruby 1.9.1_0 tag). there are build issues usi…
29 #
511dc44a » Laurent Sansonetti
2008-02-25 initial import
30 class PrettyPrint
31
32 # This is a convenience method which is same as follows:
8f211620 » richkilmer
2009-03-02 bring lib up to r22701 (ruby 1.9.1_0 tag). there are build issues usi…
33 #
511dc44a » Laurent Sansonetti
2008-02-25 initial import
34 # begin
35 # q = PrettyPrint.new(output, maxwidth, newline, &genspace)
36 # ...
37 # q.flush
38 # output
39 # end
8f211620 » richkilmer
2009-03-02 bring lib up to r22701 (ruby 1.9.1_0 tag). there are build issues usi…
40 #
511dc44a » Laurent Sansonetti
2008-02-25 initial import
41 def PrettyPrint.format(output='', maxwidth=79, newline="\n", genspace=lambda {|n| ' ' * n})
42 q = PrettyPrint.new(output, maxwidth, newline, &genspace)
43 yield q
44 q.flush
45 output
46 end
47
48 # This is similar to PrettyPrint::format but the result has no breaks.
49 #
50 # +maxwidth+, +newline+ and +genspace+ are ignored.
51 #
52 # The invocation of +breakable+ in the block doesn't break a line and is
53 # treated as just an invocation of +text+.
54 #
55 def PrettyPrint.singleline_format(output='', maxwidth=nil, newline=nil, genspace=nil)
56 q = SingleLine.new(output)
57 yield q
58 output
59 end
60
61 # Creates a buffer for pretty printing.
62 #
63 # +output+ is an output target. If it is not specified, '' is assumed. It
64 # should have a << method which accepts the first argument +obj+ of
65 # PrettyPrint#text, the first argument +sep+ of PrettyPrint#breakable, the
66 # first argument +newline+ of PrettyPrint.new, and the result of a given
67 # block for PrettyPrint.new.
68 #
69 # +maxwidth+ specifies maximum line length. If it is not specified, 79 is
70 # assumed. However actual outputs may overflow +maxwidth+ if long
71 # non-breakable texts are provided.
72 #
73 # +newline+ is used for line breaks. "\n" is used if it is not specified.
74 #
75 # The block is used to generate spaces. {|width| ' ' * width} is used if it
76 # is not given.
77 #
78 def initialize(output='', maxwidth=79, newline="\n", &genspace)
79 @output = output
80 @maxwidth = maxwidth
81 @newline = newline
82 @genspace = genspace || lambda {|n| ' ' * n}
83
84 @output_width = 0
85 @buffer_width = 0
86 @buffer = []
87
88 root_group = Group.new(0)
89 @group_stack = [root_group]
90 @group_queue = GroupQueue.new(root_group)
91 @indent = 0
92 end
93 attr_reader :output, :maxwidth, :newline, :genspace
94 attr_reader :indent, :group_queue
95
96 def current_group
97 @group_stack.last
98 end
99
100 # first? is a predicate to test the call is a first call to first? with
101 # current group.
102 #
103 # It is useful to format comma separated values as:
104 #
105 # q.group(1, '[', ']') {
106 # xxx.each {|yyy|
107 # unless q.first?
108 # q.text ','
109 # q.breakable
110 # end
111 # ... pretty printing yyy ...
112 # }
113 # }
114 #
115 # first? is obsoleted in 1.8.2.
116 #
117 def first?
118 warn "PrettyPrint#first? is obsoleted at 1.8.2."
119 current_group.first?
120 end
121
122 def break_outmost_groups
123 while @maxwidth < @output_width + @buffer_width
124 return unless group = @group_queue.deq
125 until group.breakables.empty?
126 data = @buffer.shift
127 @output_width = data.output(@output, @output_width)
128 @buffer_width -= data.width
129 end
130 while !@buffer.empty? && Text === @buffer.first
131 text = @buffer.shift
132 @output_width = text.output(@output, @output_width)
133 @buffer_width -= text.width
134 end
135 end
136 end
137
138 # This adds +obj+ as a text of +width+ columns in width.
139 #
140 # If +width+ is not specified, obj.length is used.
141 #
142 def text(obj, width=obj.length)
143 if @buffer.empty?
144 @output << obj
145 @output_width += width
146 else
147 text = @buffer.last
148 unless Text === text
149 text = Text.new
150 @buffer << text
151 end
152 text.add(obj, width)
153 @buffer_width += width
154 break_outmost_groups
155 end
156 end
157
158 def fill_breakable(sep=' ', width=sep.length)
159 group { breakable sep, width }
160 end
161
162 # This tells "you can break a line here if necessary", and a +width+\-column
163 # text +sep+ is inserted if a line is not broken at the point.
164 #
165 # If +sep+ is not specified, " " is used.
166 #
167 # If +width+ is not specified, +sep.length+ is used. You will have to
168 # specify this when +sep+ is a multibyte character, for example.
169 #
170 def breakable(sep=' ', width=sep.length)
171 group = @group_stack.last
172 if group.break?
173 flush
174 @output << @newline
175 @output << @genspace.call(@indent)
176 @output_width = @indent
177 @buffer_width = 0
178 else
179 @buffer << Breakable.new(sep, width, self)
180 @buffer_width += width
181 break_outmost_groups
182 end
183 end
184
185 # Groups line break hints added in the block. The line break hints are all
186 # to be used or not.
187 #
188 # If +indent+ is specified, the method call is regarded as nested by
189 # nest(indent) { ... }.
190 #
191 # If +open_obj+ is specified, <tt>text open_obj, open_width</tt> is called
192 # before grouping. If +close_obj+ is specified, <tt>text close_obj,
193 # close_width</tt> is called after grouping.
194 #
195 def group(indent=0, open_obj='', close_obj='', open_width=open_obj.length, close_width=close_obj.length)
196 text open_obj, open_width
197 group_sub {
198 nest(indent) {
199 yield
200 }
201 }
202 text close_obj, close_width
203 end
204
205 def group_sub
206 group = Group.new(@group_stack.last.depth + 1)
207 @group_stack.push group
208 @group_queue.enq group
209 begin
210 yield
211 ensure
212 @group_stack.pop
213 if group.breakables.empty?
214 @group_queue.delete group
215 end
216 end
217 end
218
219 # Increases left margin after newline with +indent+ for line breaks added in
220 # the block.
221 #
222 def nest(indent)
223 @indent += indent
224 begin
225 yield
226 ensure
227 @indent -= indent
228 end
229 end
230
231 # outputs buffered data.
232 #
233 def flush
234 @buffer.each {|data|
235 @output_width = data.output(@output, @output_width)
236 }
237 @buffer.clear
238 @buffer_width = 0
239 end
240
241 class Text
242 def initialize
243 @objs = []
244 @width = 0
245 end
246 attr_reader :width
247
248 def output(out, output_width)
249 @objs.each {|obj| out << obj}
250 output_width + @width
251 end
252
253 def add(obj, width)
254 @objs << obj
255 @width += width
256 end
257 end
258
259 class Breakable
260 def initialize(sep, width, q)
261 @obj = sep
262 @width = width
263 @pp = q
264 @indent = q.indent
265 @group = q.current_group
266 @group.breakables.push self
267 end
268 attr_reader :obj, :width, :indent
269
270 def output(out, output_width)
271 @group.breakables.shift
272 if @group.break?
273 out << @pp.newline
274 out << @pp.genspace.call(@indent)
275 @indent
276 else
277 @pp.group_queue.delete @group if @group.breakables.empty?
278 out << @obj
279 output_width + @width
280 end
281 end
282 end
283
284 class Group
285 def initialize(depth)
286 @depth = depth
287 @breakables = []
288 @break = false
289 end
290 attr_reader :depth, :breakables
291
292 def break
293 @break = true
294 end
295
296 def break?
297 @break
298 end
299
300 def first?
301 if defined? @first
302 false
303 else
304 @first = false
305 true
306 end
307 end
308 end
309
310 class GroupQueue
311 def initialize(*groups)
312 @queue = []
313 groups.each {|g| enq g}
314 end
315
316 def enq(group)
317 depth = group.depth
318 @queue << [] until depth < @queue.length
319 @queue[depth] << group
320 end
321
322 def deq
323 @queue.each {|gs|
324 (gs.length-1).downto(0) {|i|
325 unless gs[i].breakables.empty?
326 group = gs.slice!(i, 1).first
327 group.break
328 return group
329 end
330 }
331 gs.each {|group| group.break}
332 gs.clear
333 }
334 return nil
335 end
336
337 def delete(group)
338 @queue[group.depth].delete(group)
339 end
340 end
341
342 class SingleLine
343 def initialize(output, maxwidth=nil, newline=nil)
344 @output = output
345 @first = [true]
346 end
347
348 def text(obj, width=nil)
349 @output << obj
350 end
351
352 def breakable(sep=' ', width=nil)
353 @output << sep
354 end
355
356 def nest(indent)
357 yield
358 end
359
360 def group(indent=nil, open_obj='', close_obj='', open_width=nil, close_width=nil)
361 @first.push true
362 @output << open_obj
363 yield
364 @output << close_obj
365 @first.pop
366 end
367
368 def flush
369 end
370
371 def first?
372 result = @first[-1]
373 @first[-1] = false
374 result
375 end
376 end
377 end
Something went wrong with that request. Please try again.