# 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