Skip to content

Instantly share code, notes, and snippets.

@asterite
Created November 26, 2016 14:43
Show Gist options
  • Save asterite/d1b9b8dd1ed11a5e2018973ab740b778 to your computer and use it in GitHub Desktop.
Save asterite/d1b9b8dd1ed11a5e2018973ab740b778 to your computer and use it in GitHub Desktop.
# ### References
#
# * [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 PrettyPrinter
# :nodoc:
private alias Element = Break | Text | Group
# :nodoc:
private record Break, string : String do
def fit(available)
available -= string.size
available
end
end
# :nodoc:
private record Text, string : String do
def fit(available)
available -= string.size
available
end
end
# :nodoc:
private record Group, elements : Array(Element), indent : Int32 do
def initialize(@indent : Int32)
@elements = [] of Element
end
def <<(element)
@elements << element
end
def output(io, width, available = width, indent = @indent)
# Check if self fits in the available space.
# If not, all my breaks will be appended as newlines.
self_fits = fit(available) >= 0
@elements.each_with_index do |element, i|
case element
when Group
available = element.output(io, width, available, indent + element.indent)
when Text
io << element.string
available -= element.string.size
when Break
unless self_fits
io.puts
indent.times { io << " " }
available = width - indent
next
end
# After a break, check if the next text o group
# fits in the available space.
# If not, we append a newline.
next_element = @elements[i + 1]?
fits =
case next_element
when Group
next_element.fit(available - 1) >= 0
when Text
available >= next_element.string.size
else
available > 0
end
if fits
io << element.string
available -= element.string.size
else
io.puts
indent.times { io << " " }
available = width - indent
end
end
end
available
end
def fit(available)
@elements.each do |element|
available = element.fit(available)
break if available < 0
end
available
end
end
private getter current_group
def initialize
@current_group = Group.new(0)
end
# Starts a group.
def group : Nil
subgroup(0) do
yield
end
end
# Starts a nesting with the given indentation.
def nest(indent = 1) : Nil
subgroup(indent) do
yield
nil
end
end
private def subgroup(indent) : Nil
group = Group.new(indent)
@current_group << group
old_group = @current_group
@current_group = group
yield
@current_group = old_group
end
# Appends a text object
def text(obj) : Nil
current_group << Text.new(obj.to_s)
end
# Appends a break.
# The break can turn into a newline if needed.
def break(text = " ") : Nil
current_group << Break.new(text)
end
# Starts a nesting that will be surrounded.
def surround(left, right, left_break = "", right_break = "") : Nil
text left
nest do
self.break left_break if left_break
yield
nil
end
self.break right_break if right_break
text right
end
# Appends a list of elements separated by commas and
# surroundeded by *left* and *right*, yielding each element
# in the list to the block.
def list(left, elements, right, left_break = nil, right_break = nil) : Nil
surround(left, right, left_break, right_break) do
elements.each_with_index do |elem, i|
comma if i > 0
yield elem
nil
end
end
end
# Appends a list of elements separated by commas and
# surroundeded by *left* and *right*.
def list(left, elements, right, left_break = nil, right_break = nil) : Nil
list(left, elements, right, left_break, right_break) do |elem|
elem.pretty_print(self)
nil
end
end
# Appends a text comma followed by a break.
def comma : Nil
text ","
self.break
end
protected def output(io, width, indent)
@current_group.output(io, width, indent: indent)
end
# Creates a printer, yields it to the block,
# and then outputs the result to the given IO
# limited to the given *width* and starting with
# the given *indent*ation.
def self.print(io : IO, width : Int32, indent = 0)
printer = new
yield printer
printer.output(io, width, indent: indent)
end
# Pretty prints *obj* into *io* with the given
# *width* as a limit and starting with
# the given *indent*ation.
def self.print(obj, io : IO, width : Int32, indent = 0)
print(io, width, indent) do |printer|
obj.pretty_print(printer)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment