/
pretty_print.cr
307 lines (269 loc) · 7.33 KB
/
pretty_print.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# This class implements a pretty printing algorithm.
# It finds line breaks and nice indentations for grouped structure.
#
# ### References
#
# * [Ruby's prettyprint.rb](https://github.com/ruby/ruby/blob/master/lib/prettyprint.rb)
# * [Christian Lindig, Strictly Pretty, March 2000](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.34.2200)
# * [Philip Wadler, A prettier printer, March 1998](http://homepages.inf.ed.ac.uk/wadler/topics/language-design.html#prettier)
class PrettyPrint
protected getter group_queue
protected getter newline
protected getter indent
# Creates a new pretty printer that will write to the given *output*
# and be capped at *maxwidth*.
def initialize(@output : IO, @maxwidth = 79, @newline = "\n", @indent = 0)
@output_width = @indent
@buffer_width = 0
# Buffer of object that can't yet be printed to
# the output because we don't know if the current
# group overflows maxwidth or not
@buffer = Deque(Text | Breakable).new
root_group = Group.new(0)
# All groups being pushed by `group` calls
@group_stack = [] of Group
@group_stack << root_group
# Queue of array of groups (one array per group level)
# that are not yet breakable
@group_queue = GroupQueue.new
@group_queue.enq root_group
end
protected def current_group
@group_stack.last
end
# Checks if the current output width plus the
# total width accumulated in buffer objects exceeds
# the maximum allowed width. If so, it means that
# all groups until the first break must be broken
# into newlines, and all breakables and texts until
# that point can be printed.
protected def break_outmost_groups
while @maxwidth < @output_width + @buffer_width
return unless group = @group_queue.deq
until group.breakables.empty?
data = @buffer.shift
@output_width = data.output(@output, @output_width)
@buffer_width -= data.width
end
while !@buffer.empty? && @buffer.first.is_a?(Text)
text = @buffer.shift.as(Text)
@output_width = text.output(@output, @output_width)
@buffer_width -= text.width
end
end
end
# Appends a text element.
def text(obj) : Nil
obj = obj.to_s
width = obj.size
return if width == 0
if @buffer.empty?
@output << obj
@output_width += width
else
text = @buffer.last
unless text.is_a?(Text)
text = Text.new
@buffer << text
end
text.add(obj, width)
@buffer_width += width
break_outmost_groups
end
end
# Appends an element that can turn into a newline if necessary.
def breakable(sep = " ") : Nil
width = sep.size
group = @group_stack.last
if group.break?
flush
@output << @newline
@indent.times { @output << ' ' }
@output_width = @indent
@buffer_width = 0
else
@buffer << Breakable.new(sep, width, self)
@buffer_width += width
break_outmost_groups
end
end
# Similar to `#breakable` except
# the decision to break or not is determined individually.
def fill_breakable(sep = " ") : Nil
group { breakable sep }
end
# Creates a group of objects. Inside a group all breakable
# objects are either turned into newlines or are output
# as is, depending on the available width.
def group(indent = 0, open_obj = "", close_obj = "")
text open_obj
group_sub do
nest(indent) do
yield
end
end
text close_obj
end
private def group_sub
group = Group.new(@group_stack.last.depth + 1)
@group_stack.push group
@group_queue.enq group
begin
yield
ensure
@group_stack.pop
if group.breakables.empty?
@group_queue.delete group
end
end
end
# Increases the indentation for breakables inside the current group.
def nest(indent = 1)
@indent += indent
begin
yield
ensure
@indent -= indent
end
end
# Same as:
#
# ```
# text ","
# breakable
# ```
def comma : Nil
text ","
breakable
end
# Appends a group that is surrounded by the given *left* and *right*
# objects, and optionally is surrounded by the given breakable
# objects.
def surround(left, right, left_break = "", right_break = "") : Nil
group(1, left, right) do
breakable left_break if left_break
yield
breakable right_break if right_break
end
end
# Appends a list of elements surrounded by *left* and *right*
# and separated by commas, yielding each element to the given block.
def list(left, elements, right) : Nil
group(1, left, right) do
elements.each_with_index do |elem, i|
comma if i > 0
yield elem
end
end
end
# Appends a list of elements surrounded by *left* and *right*
# and separated by commas.
def list(left, elements, right) : Nil
list(left, elements, right) do |elem|
elem.pretty_print(self)
end
end
# Outputs any buffered data.
def flush : Nil
@buffer.each do |data|
@output_width = data.output(@output, @output_width)
end
@buffer.clear
@buffer_width = 0
@output.flush
end
private class Text
getter width
def initialize
@objs = [] of String
@width = 0
end
def output(io, output_width)
@objs.each { |obj| io << obj }
output_width + @width
end
def add(obj, width)
@objs << obj.to_s
@width += width
end
end
private class Breakable
@indent : Int32
@group : Group
getter width
def initialize(@obj : String, @width : Int32, @pp : PrettyPrint)
@indent = @pp.indent
@group = @pp.current_group
@group.breakables.push self
end
def output(io, output_width)
@group.breakables.shift
if @group.break?
io << @pp.newline
@indent.times { io << ' ' }
@indent
else
@pp.group_queue.delete @group if @group.breakables.empty?
io << @obj
output_width + @width
end
end
end
private class Group
getter depth
getter breakables
getter? :break
def initialize(@depth : Int32)
@breakables = Deque(Breakable).new
@break = false
end
def break : Nil
@break = true
end
end
private class GroupQueue
def initialize
@queue = [] of Array(Group)
end
def enq(group)
depth = group.depth
until depth < @queue.size
@queue << [] of Group
end
@queue[depth] << group
end
def deq
@queue.each do |gs|
(gs.size - 1).downto(0) do |i|
unless gs[i].breakables.empty?
group = gs.delete_at(i)
group.break
return group
end
end
gs.each &.break
gs.clear
end
nil
end
def delete(group)
@queue[group.depth].delete(group)
end
end
# Pretty prints *obj* into *io* with the given
# *width* as a limit and starting with
# the given *indent*ation.
def self.format(obj, io : IO, width : Int32, newline = "\n", indent = 0)
format(io, width, newline, indent) do |printer|
obj.pretty_print(printer)
end
end
# Creates a pretty printer and yields it to the block,
# appending any output to the given *io*.
def self.format(io : IO, width : Int32, newline = "\n", indent = 0)
printer = PrettyPrint.new(io, width, newline, indent)
yield printer
printer.flush
io
end
end