Created
November 26, 2016 14:43
-
-
Save asterite/d1b9b8dd1ed11a5e2018973ab740b778 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ### 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