Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 378 lines (342 sloc) 9.086 kb
511dc44 initial import
Laurent Sansonetti authored
1 # This class implements a pretty printing algorithm. It finds line breaks and
2 # nice indentations for grouped structure.
8f21162 @richkilmer bring lib up to r22701 (ruby 1.9.1_0 tag). there are build issues us…
richkilmer authored
3 #
511dc44 initial import
Laurent Sansonetti authored
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?
8f21162 @richkilmer bring lib up to r22701 (ruby 1.9.1_0 tag). there are build issues us…
richkilmer authored
19 #
511dc44 initial import
Laurent Sansonetti authored
20 # == References
21 # Christian Lindig, Strictly Pretty, March 2000,
22 # http://www.st.cs.uni-sb.de/~lindig/papers/#pretty
8f21162 @richkilmer bring lib up to r22701 (ruby 1.9.1_0 tag). there are build issues us…
richkilmer authored
23 #
511dc44 initial import
Laurent Sansonetti authored
24 # Philip Wadler, A prettier printer, March 1998,
25 # http://homepages.inf.ed.ac.uk/wadler/topics/language-design.html#prettier
8f21162 @richkilmer bring lib up to r22701 (ruby 1.9.1_0 tag). there are build issues us…
richkilmer authored
26 #
511dc44 initial import
Laurent Sansonetti authored
27 # == Author
28 # Tanaka Akira <akr@m17n.org>
8f21162 @richkilmer bring lib up to r22701 (ruby 1.9.1_0 tag). there are build issues us…
richkilmer authored
29 #
511dc44 initial import
Laurent Sansonetti authored
30 class PrettyPrint
31
32 # This is a convenience method which is same as follows:
8f21162 @richkilmer bring lib up to r22701 (ruby 1.9.1_0 tag). there are build issues us…
richkilmer authored
33 #
511dc44 initial import
Laurent Sansonetti authored
34 # begin
35 # q = PrettyPrint.new(output, maxwidth, newline, &genspace)
36 # ...
37 # q.flush
38 # output
39 # end
8f21162 @richkilmer bring lib up to r22701 (ruby 1.9.1_0 tag). there are build issues us…
richkilmer authored
40 #
511dc44 initial import
Laurent Sansonetti authored
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.