diff --git a/.gitignore b/.gitignore
index f6e4e0d..06ef046 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
test.rb
+profile.json
diff --git a/Gemfile b/Gemfile
index 37909f4..b0c29c2 100644
--- a/Gemfile
+++ b/Gemfile
@@ -5,3 +5,5 @@ source "https://rubygems.org"
gemspec
gem "prism"
+gem "vernier"
+gem "profile-viewer"
diff --git a/Gemfile.lock b/Gemfile.lock
index a483e4f..c12fcfc 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -8,8 +8,14 @@ PATH
GEM
remote: https://rubygems.org/
specs:
+ optparse (0.5.0)
prettier_print (1.2.1)
prism (0.30.0)
+ profile-viewer (0.0.2)
+ optparse
+ webrick
+ vernier (1.0.1)
+ webrick (1.8.1)
PLATFORMS
arm64-darwin-22
@@ -17,7 +23,9 @@ PLATFORMS
DEPENDENCIES
prism
+ profile-viewer
syntax_tree-prism!
+ vernier
BUNDLED WITH
2.4.13
diff --git a/bin/bench b/bin/bench
new file mode 100755
index 0000000..4f17212
--- /dev/null
+++ b/bin/bench
@@ -0,0 +1,17 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "benchmark"
+
+$:.unshift(File.expand_path("../lib", __dir__))
+require "syntax_tree/prism"
+
+filepaths = Dir[ARGV.first]
+puts "Formatting #{filepaths.length} files"
+
+Benchmark.bmbm do |x|
+ x.report("format") do
+ filepaths.each { |filepath| Prism.parse_file(filepath).format }
+ end
+end
diff --git a/bin/format b/bin/format
index 00daa92..a8a5885 100755
--- a/bin/format
+++ b/bin/format
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require "bundler/setup"
diff --git a/bin/profile b/bin/profile
new file mode 100755
index 0000000..7b8948b
--- /dev/null
+++ b/bin/profile
@@ -0,0 +1,13 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "vernier"
+
+$:.unshift(File.expand_path("../lib", __dir__))
+require "syntax_tree/prism"
+
+results = Dir[ARGV.first].map { |filepath| Prism.parse_file(filepath) }
+puts "Profiling #{results.length} files"
+
+Vernier.trace(out: "profile.json") { results.each(&:format) }
diff --git a/lib/syntax_tree/prism.rb b/lib/syntax_tree/prism.rb
index 434d5fa..6acf956 100644
--- a/lib/syntax_tree/prism.rb
+++ b/lib/syntax_tree/prism.rb
@@ -1,32 +1,231 @@
# frozen_string_literal: true
-require_relative "prism/prettier_print"
require "prism"
module Prism
- class Format < PrettierPrint
+ # Philip Wadler, A prettier printer, March 1998
+ # https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf
+ class Format
+ # A node in the print tree that represents aligning nested nodes to a
+ # certain prefix width or string.
+ class Align
+ attr_reader :indent, :contents
+
+ def initialize(indent, contents)
+ @indent = indent
+ @contents = contents
+ end
+
+ def type
+ :align
+ end
+ end
+
+ # A node in the print tree that represents a place in the buffer that the
+ # content can be broken onto multiple lines.
+ class Breakable
+ attr_reader :separator, :width, :force, :indent
+
+ def initialize(separator, width, force, indent)
+ @separator = separator
+ @width = width
+ @force = force
+ @indent = indent
+ end
+
+ def type
+ :breakable
+ end
+ end
+
+ # Below here are the most common combination of options that are created
+ # when creating new breakables. They are here to cut down on some
+ # allocations.
+ BREAKABLE_SPACE = Breakable.new(" ", 1, false, true).freeze
+ BREAKABLE_EMPTY = Breakable.new("", 0, false, true).freeze
+ BREAKABLE_FORCE = Breakable.new(" ", 1, true, true).freeze
+ BREAKABLE_RETURN = Breakable.new(" ", 1, true, false).freeze
+
+ # A node in the print tree that forces the surrounding group to print out in
+ # the "break" mode as opposed to the "flat" mode. Useful for when you need
+ # to force a newline into a group.
+ class BreakParent
+ def type
+ :break_parent
+ end
+ end
+
+ # Since there's really no difference in these instances, just using the same
+ # one saves on some allocations.
+ BREAK_PARENT = BreakParent.new.freeze
+
+ # A node in the print tree that represents a group of items which the printer
+ # should try to fit onto one line. This is the basic command to tell the
+ # printer when to break. Groups are usually nested, and the printer will try
+ # to fit everything on one line, but if it doesn't fit it will break the
+ # outermost group first and try again. It will continue breaking groups until
+ # everything fits (or there are no more groups to break).
+ class Group
+ attr_reader :contents, :break
+
+ def initialize(contents)
+ @contents = contents
+ @break = false
+ end
+
+ def break!
+ @break = true
+ end
+
+ def type
+ :group
+ end
+ end
+
+ # A node in the print tree that represents printing one thing if the
+ # surrounding group node is broken and another thing if the surrounding group
+ # node is flat.
+ class IfBreak
+ attr_reader :break_contents, :flat_contents
+
+ def initialize(break_contents, flat_contents)
+ @break_contents = break_contents
+ @flat_contents = flat_contents
+ end
+
+ def type
+ :if_break
+ end
+ end
+
+ # A node in the print tree that is a variant of the Align node that indents
+ # its contents by one level.
+ class Indent
+ attr_reader :contents
+
+ def initialize(contents)
+ @contents = contents
+ end
+
+ def type
+ :indent
+ end
+ end
+
+ # A node in the print tree that has its own special buffer for implementing
+ # content that should flush before any newline.
+ #
+ # Useful for implementating trailing content, as it's not always practical to
+ # constantly check where the line ends to avoid accidentally printing some
+ # content after a line suffix node.
+ class LineSuffix
+ attr_reader :priority, :contents
+
+ def initialize(priority, contents)
+ @priority = priority
+ @contents = contents
+ end
+
+ def type
+ :line_suffix
+ end
+ end
+
+ # A node in the print tree that represents trimming all of the indentation of
+ # the current line, in the rare case that you need to ignore the indentation
+ # that you've already created. This node should be placed after a Breakable.
+ class Trim
+ def type
+ :trim
+ end
+ end
+
+ # Since all of the instances here are the same, we can reuse the same one to
+ # cut down on allocations.
+ TRIM = Trim.new.freeze
+
+ # Refine string so that we can consistently call #type in case statements and
+ # treat all of the nodes in the tree as homogenous.
+ using Module.new {
+ refine String do
+ def type
+ :string
+ end
+ end
+ }
+
+ # There are two modes in printing, break and flat. When we're in break mode,
+ # any lines will use their newline, any if-breaks will use their break
+ # contents, etc.
+ MODE_BREAK = 1
+
+ # This is another print mode much like MODE_BREAK. When we're in flat mode, we
+ # attempt to print everything on one line until we either hit a broken group,
+ # a forced line, or the maximum width.
+ MODE_FLAT = 2
+
+ # The default indentation for printing is zero, assuming that the code starts
+ # at the top level. That can be changed if desired to start from a different
+ # indentation level.
+ DEFAULT_INDENTATION = 0
+
COMMENT_PRIORITY = 1
HEREDOC_PRIORITY = 2
attr_reader :source, :stack
attr_reader :quote
- def initialize(source, *args, quote: "\"")
+ # This is an output buffer that contains the various parts of the printed
+ # source code.
+ attr_reader :buffer
+
+ # The maximum width of a line, before it is separated in to a newline.
+ attr_reader :maxwidth
+
+ # The stack of groups that are being printed.
+ attr_reader :groups
+
+ # The current array of contents that calls to methods that generate print
+ # tree nodes will append to.
+ attr_reader :target
+
+ def initialize(source, buffer = [], maxwidth = 80, quote = "\"")
@source = source
@stack = []
-
+ @buffer = buffer
+ @maxwidth = maxwidth
@quote = quote
- super(*args)
+ contents = []
+ @groups = [Group.new(contents)]
+ @target = contents
+ end
+
+ # The main API for this visitor.
+ def format
+ flush
+ buffer.join
+ end
+
+ # A convenience method used by a lot of the print tree node builders that
+ # temporarily changes the target that the builders will append to.
+ def with_target(target)
+ previous_target, @target = @target, target
+ yield
+ @target = previous_target
end
+ # --------------------------------------------------------------------------
+ # Visit methods
+ # --------------------------------------------------------------------------
+
# alias $foo $bar
# ^^^^^^^^^^^^^^^
def visit_alias_global_variable_node(node)
group do
text("alias ")
visit(node.new_name)
- nest(6) do
+ align(6) do
breakable_space
visit(node.old_name)
end
@@ -49,7 +248,7 @@ def visit_alias_method_node(node)
visit(new_name)
end
- nest(6) do
+ align(6) do
breakable_space
if old_name.is_a?(SymbolNode)
text(old_name.value)
@@ -299,21 +498,21 @@ def visit_begin_node(node)
visit(statements) unless statements.nil?
unless rescue_clause.nil?
- nest(-2) do
+ align(-2) do
breakable_force
visit(rescue_clause)
end
end
unless else_clause.nil?
- nest(-2) do
+ align(-2) do
breakable_force
visit(else_clause)
end
end
unless ensure_clause.nil?
- nest(-2) do
+ align(-2) do
breakable_force
visit(ensure_clause)
end
@@ -459,7 +658,7 @@ def visit_block_parameter_node(node)
visit_location(node.operator_loc)
if name
- nest(1) do
+ align(1) do
breakable_empty
text(name.name)
end
@@ -720,7 +919,7 @@ def visit_call_node(node)
*rest, last = chain
doc =
- nest(0) do
+ align(0) do
visit(rest.shift) unless rest.first.is_a?(CallNode)
rest.each do |node|
@@ -933,7 +1132,7 @@ def visit_call_node(node)
end
end
- nest(position > (maxwidth / 2) ? 0 : position) do
+ align(position > (maxwidth / 2) ? 0 : position) do
seplist(arguments) { |argument| visit(argument) }
end
end
@@ -992,7 +1191,7 @@ def visit_case_node(node)
if predicate
text(" ")
- nest(5) { visit(predicate) }
+ align(5) { visit(predicate) }
end
end
@@ -1358,7 +1557,7 @@ def visit_flip_flop_node(node)
if right
indent do
- breakable
+ breakable_space
visit(right)
end
end
@@ -1539,7 +1738,7 @@ def visit_if_node(node)
group do
visit_location(if_keyword_loc)
text(" ")
- nest(6) { visit(node.predicate) }
+ align(6) { visit(node.predicate) }
if consequent
visit_body(statements, [], true)
@@ -1632,7 +1831,7 @@ def visit_if_node(node)
private def visit_ternary_node_break(node, truthy, falsy)
group do
text("if ")
- nest(3) { visit(node.predicate) }
+ align(3) { visit(node.predicate) }
end
indent do
@@ -1660,7 +1859,7 @@ def visit_if_node(node)
group do
visit_location(node.if_keyword_loc)
text(" ")
- nest(3) { visit(node.predicate) }
+ align(3) { visit(node.predicate) }
end
if consequent
@@ -1706,7 +1905,7 @@ def visit_in_node(node)
group do
text("in ")
- nest(3) { visit(node.pattern) }
+ align(3) { visit(node.pattern) }
if statements
indent do
@@ -2235,7 +2434,7 @@ def visit_nil_node(node)
def visit_no_keywords_parameter_node(node)
group do
visit_location(node.operator_loc)
- nest(2) do
+ align(2) do
breakable_empty
text("nil")
end
@@ -2291,7 +2490,7 @@ def visit_parameters_node(node)
parameters = node.compact_child_nodes
implicit_rest = parameters.pop if parameters.last.is_a?(ImplicitRestNode)
- nest(0) do
+ align(0) do
seplist(parameters) { |parameter| visit(parameter) }
visit(implicit_rest) if implicit_rest
end
@@ -2476,7 +2675,7 @@ def visit_rescue_node(node)
if exceptions.any?
text(" ")
- nest(7) { seplist(exceptions) { |exception| visit(exception) } }
+ align(7) { seplist(exceptions) { |exception| visit(exception) } }
elsif reference.nil?
text(" StandardError")
end
@@ -2522,7 +2721,7 @@ def visit_rest_parameter_node(node)
if name
group do
visit_location(node.operator_loc)
- nest(1) do
+ align(1) do
breakable_empty
text(node.name.name)
end
@@ -2615,7 +2814,7 @@ def visit_splat_node(node)
text("*")
operator_loc.comments.each { |comment| visit_comment(comment) }
- nest(1) do
+ align(1) do
breakable_empty
visit(expression)
end
@@ -2704,7 +2903,7 @@ def visit_super_node(node)
text(")")
elsif arguments.any?
text(" ")
- nest(6) { seplist(arguments) { |argument| visit(argument) } }
+ align(6) { seplist(arguments) { |argument| visit(argument) } }
end
if block
@@ -2731,7 +2930,7 @@ def visit_true_node(node)
def visit_undef_node(node)
group do
text("undef ")
- nest(6) do
+ align(6) do
seplist(node.names) do |name|
if name.is_a?(SymbolNode)
text(name.value)
@@ -2786,7 +2985,7 @@ def visit_unless_node(node)
group do
visit_location(node.keyword_loc)
text(" ")
- nest(3) { visit(node.predicate) }
+ align(3) { visit(node.predicate) }
end
if consequent
@@ -2842,7 +3041,7 @@ def visit_until_node(node)
private def visit_until_node_break(node)
visit_location(node.keyword_loc)
text(" ")
- nest(6) { visit(node.predicate) }
+ align(6) { visit(node.predicate) }
visit_body(node.statements, node.closing_loc&.comments || [], false)
breakable_space
text("end")
@@ -2866,7 +3065,7 @@ def visit_when_node(node)
group do
group do
text("when ")
- nest(5) do
+ align(5) do
seplist(conditions, -> { group { comma_breakable } }) do |condition|
visit(condition)
end
@@ -2931,7 +3130,7 @@ def visit_while_node(node)
private def visit_while_node_break(node)
visit_location(node.keyword_loc)
text(" ")
- nest(6) { visit(node.predicate) }
+ align(6) { visit(node.predicate) }
visit_body(node.statements, node.closing_loc&.comments || [], false)
breakable_space
text("end")
@@ -2987,9 +3186,9 @@ def visit_yield_node(node)
private
- ############################################################################
- # Helper methods #
- ############################################################################
+ # --------------------------------------------------------------------------
+ # Helper methods
+ # --------------------------------------------------------------------------
# Returns whether or not the given statement is an access control statement.
# Truthfully, we can't actually tell this for sure without performing method
@@ -3141,9 +3340,9 @@ def quotes_normalize(content, enclosing)
end
end
- ############################################################################
- # Visit methods #
- ############################################################################
+ # --------------------------------------------------------------------------
+ # Visit helpers
+ # --------------------------------------------------------------------------
# Visit a node and format it, including any comments that are found around
# it that are attached to its location.
@@ -3221,7 +3420,7 @@ def visit_comment(comment)
break_parent
end
else
- breakable
+ breakable_space
text(comment.location.slice)
break_parent
end
@@ -3262,14 +3461,11 @@ def visit_elements_spaced(elements, comments)
# Visit a heredoc node, and format it with the given parts.
def visit_heredoc(node, parts)
- # This is a very specific behavior where you want to force a newline, but
- # don't want to force the break parent.
- separator = PrettierPrint::Breakable.new(" ", 1, true, false)
- opening = node.opening
-
# If the heredoc is indented, then we're going to need to reintroduce the
# indentation to the parts of the heredoc.
indent = ""
+
+ opening = node.opening
if opening[2] == "~"
parts.each do |part|
if part.is_a?(StringNode) && !part.content.start_with?("\n")
@@ -3283,7 +3479,7 @@ def visit_heredoc(node, parts)
text(opening)
line_suffix(HEREDOC_PRIORITY) do
group do
- target << separator
+ target << BREAKABLE_RETURN
parts.each_with_index do |part, index|
case part.type
@@ -3295,13 +3491,13 @@ def visit_heredoc(node, parts)
if first
first = false
else
- target << separator
+ target << BREAKABLE_RETURN
end
text(line)
end
- target << separator if value.end_with?("\n")
+ target << BREAKABLE_RETURN if value.end_with?("\n")
else
text(indent)
visit(part)
@@ -3452,7 +3648,7 @@ def visit_prefix(operator_loc, value)
if value
group do
visit_location(operator_loc)
- nest(operator_loc.length) do
+ align(operator_loc.length) do
breakable_empty
visit(value)
end
@@ -3597,6 +3793,557 @@ def visit_write(operator_loc, value)
end
end
end
+
+ # --------------------------------------------------------------------------
+ # Printing algorithm
+ # --------------------------------------------------------------------------
+
+ # Flushes all of the generated print tree onto the output buffer.
+ def flush
+ # First, get the root group, since we placed one at the top to begin with.
+ doc = groups.first
+
+ # This represents how far along the current line we are. It gets reset
+ # back to 0 when we encounter a newline.
+ position = 0
+
+ # This is our command stack. A command consists of a triplet of an
+ # indentation level, the mode (break or flat), and a doc node.
+ commands = [[0, MODE_BREAK, doc]]
+
+ # This is a small optimization boolean. It keeps track of whether or not
+ # when we hit a group node we should check if it fits on the same line.
+ should_remeasure = false
+
+ # This is a separate command stack that includes the same kind of triplets
+ # as the commands variable. It is used to keep track of things that should
+ # go at the end of printed lines once the other doc nodes are accounted for.
+ # Typically this is used to implement comments.
+ line_suffixes = []
+
+ # This is a special sort used to order the line suffixes by both the
+ # priority set on the line suffix and the index it was in the original
+ # array.
+ line_suffix_sort = ->(line_suffix) do
+ [-line_suffix.last.priority, -line_suffixes.index(line_suffix)]
+ end
+
+ # This is a linear stack instead of a mutually recursive call defined on
+ # the individual doc nodes for efficiency.
+ while (indent, mode, doc = commands.pop)
+ case doc.type
+ when :string
+ buffer << doc
+ position += doc.length
+ when :group
+ if mode == MODE_FLAT && !should_remeasure
+ next_mode = doc.break ? MODE_BREAK : MODE_FLAT
+ commands += doc.contents.reverse.map { |part| [indent, next_mode, part] }
+ else
+ should_remeasure = false
+
+ if doc.break
+ commands += doc.contents.reverse.map { |part| [indent, MODE_BREAK, part] }
+ else
+ next_commands = doc.contents.reverse.map { |part| [indent, MODE_FLAT, part] }
+
+ if fits?(next_commands, commands, maxwidth - position)
+ commands += next_commands
+ else
+ commands += next_commands.map { |command| command[1] = MODE_BREAK; command }
+ end
+ end
+ end
+ when :breakable
+ if mode == MODE_FLAT
+ if doc.force
+ # This line was forced into the output even if we were in flat mode,
+ # so we need to tell the next group that no matter what, it needs to
+ # remeasure because the previous measurement didn't accurately
+ # capture the entire expression (this is necessary for nested
+ # groups).
+ should_remeasure = true
+ else
+ buffer << doc.separator
+ position += doc.width
+ next
+ end
+ end
+
+ # If there are any commands in the line suffix buffer, then we're going
+ # to flush them now, as we are about to add a newline.
+ if line_suffixes.any?
+ commands << [indent, mode, doc]
+
+ line_suffixes.sort_by(&line_suffix_sort).each do |(indent, mode, doc)|
+ commands += doc.contents.reverse.map { |part| [indent, mode, part] }
+ end
+
+ line_suffixes.clear
+ next
+ end
+
+ if !doc.indent
+ buffer << "\n"
+ position = 0
+ else
+ position -= trim!(buffer)
+ buffer << "\n"
+ buffer << " " * indent
+ position = indent
+ end
+ when :indent
+ next_indent = indent + 2
+ commands += doc.contents.reverse.map { |part| [next_indent, mode, part] }
+ when :align
+ next_indent = indent + doc.indent
+ commands += doc.contents.reverse.map { |part| [next_indent, mode, part] }
+ when :trim
+ position -= trim!(buffer)
+ when :if_break
+ if mode == MODE_BREAK && doc.break_contents.any?
+ commands += doc.break_contents.reverse.map { |part| [indent, mode, part] }
+ elsif mode == MODE_FLAT && doc.flat_contents.any?
+ commands += doc.flat_contents.reverse.map { |part| [indent, mode, part] }
+ end
+ when :line_suffix
+ line_suffixes << [indent, mode, doc]
+ when :break_parent
+ # do nothing
+ else
+ # Special case where the user has defined some way to get an extra doc
+ # node that we don't explicitly support into the list. In this case
+ # we're going to assume it's 0-width and just append it to the output
+ # buffer.
+ #
+ # This is useful behavior for putting marker nodes into the list so that
+ # you can know how things are getting mapped before they get printed.
+ buffer << doc
+ end
+
+ if commands.empty? && line_suffixes.any?
+ line_suffixes.sort_by(&line_suffix_sort).each do |(indent, mode, doc)|
+ commands.concat doc.contents.reverse.map { |part| [indent, mode, part] }
+ end
+
+ line_suffixes.clear
+ end
+ end
+ end
+
+ # This method returns a boolean as to whether or not the remaining commands
+ # fit onto the remaining space on the current line. If we finish printing
+ # all of the commands or if we hit a newline, then we return true. Otherwise
+ # if we continue printing past the remaining space, we return false.
+ def fits?(next_commands, rest_commands, remaining)
+ # This is the index in the remaining commands that we've handled so far.
+ # We reverse through the commands and add them to the stack if we've run
+ # out of nodes to handle.
+ rest_index = rest_commands.length
+
+ # This is our stack of commands, very similar to the commands list in the
+ # print method.
+ commands = [*next_commands]
+
+ # This is our output buffer, really only necessary to keep track of
+ # because we could encounter a Trim doc node that would actually add
+ # remaining space.
+ fit_buffer = []
+
+ while remaining >= 0
+ if commands.empty?
+ return true if rest_index == 0
+
+ rest_index -= 1
+ commands << rest_commands[rest_index]
+ next
+ end
+
+ indent, mode, doc = commands.pop
+
+ case doc.type
+ when :string
+ fit_buffer << doc
+ remaining -= doc.length
+ when :group
+ next_mode = doc.break ? MODE_BREAK : mode
+ commands += doc.contents.reverse.map { |part| [indent, next_mode, part] }
+ when :breakable
+ if mode == MODE_FLAT && !doc.force
+ fit_buffer << doc.separator
+ remaining -= doc.width
+ next
+ end
+
+ return true
+ when :indent
+ next_indent = indent + 2
+ commands += doc.contents.reverse.map { |part| [next_indent, mode, part] }
+ when :align
+ next_indent = indent + doc.indent
+ commands += doc.contents.reverse.map { |part| [next_indent, mode, part] }
+ when :trim
+ remaining += trim!(fit_buffer)
+ when :if_break
+ if mode == MODE_BREAK && doc.break_contents.any?
+ commands += doc.break_contents.reverse.map { |part| [indent, mode, part] }
+ elsif mode == MODE_FLAT && doc.flat_contents.any?
+ commands += doc.flat_contents.reverse.map { |part| [indent, mode, part] }
+ end
+ end
+ end
+
+ false
+ end
+
+ def trim!(buffer)
+ return 0 if buffer.empty?
+
+ trimmed = 0
+
+ while buffer.any? && buffer.last.is_a?(String) &&
+ buffer.last.match?(/\A[\t ]*\z/)
+ trimmed += buffer.pop.length
+ end
+
+ if buffer.any? && buffer.last.is_a?(String) && !buffer.last.frozen?
+ length = buffer.last.length
+ buffer.last.gsub!(/[\t ]*\z/, "")
+ trimmed += length - buffer.last.length
+ end
+
+ trimmed
+ end
+
+ # --------------------------------------------------------------------------
+ # Helper node builders
+ # --------------------------------------------------------------------------
+
+ # This method calculates the position of the text relative to the current
+ # indentation level when the doc has been printed. It's useful for
+ # determining how to align text to doc nodes that are already built into the
+ # tree.
+ def last_position(node)
+ queue = [node]
+ width = 0
+
+ while (doc = queue.shift)
+ case doc.type
+ when :string
+ width += doc.length
+ when :group, :indent, :align
+ queue = doc.contents + queue
+ when :breakable
+ width = 0
+ when :if_break
+ queue = doc.break_contents + queue
+ end
+ end
+
+ width
+ end
+
+ # This method will remove any breakables from the list of contents so that
+ # no newlines are present in the output. If a newline is being forced into
+ # the output, the replace value will be used.
+ def remove_breaks(node, replace = "; ")
+ queue = [node]
+
+ while (doc = queue.shift)
+ case doc.type
+ when :align, :indent, :group
+ doc.contents.map! { |child| remove_breaks_with(child, replace) }
+ queue += doc.contents
+ when :if_break
+ doc.flat_contents.map! { |child| remove_breaks_with(child, replace) }
+ queue += doc.flat_contents
+ end
+ end
+ end
+
+ # Remove breaks from a subtree with the given replacement string.
+ def remove_breaks_with(doc, replace)
+ case doc.type
+ when :breakable
+ doc.force ? replace : doc.separator
+ when :if_break
+ Align.new(0, doc.flat_contents)
+ else
+ doc
+ end
+ end
+
+ # Adds a separated list.
+ # The list is separated by comma with breakable space, by default.
+ #
+ # #seplist iterates the +list+ using +each+.
+ # It yields each object to the block given for #seplist.
+ # The procedure +separator_proc+ is called between each yields.
+ #
+ # If the iteration is zero times, +separator_proc+ is not called at all.
+ #
+ # If +separator_proc+ is nil or not given,
+ # +lambda { comma_breakable }+ is used.
+ #
+ # For example, following 3 code fragments has similar effect.
+ #
+ # q.seplist([1,2,3]) {|v| xxx v }
+ #
+ # q.seplist([1,2,3], lambda { q.comma_breakable }, :each) {|v| xxx v }
+ #
+ # xxx 1
+ # q.comma_breakable
+ # xxx 2
+ # q.comma_breakable
+ # xxx 3
+ def seplist(list, sep = nil) # :yield: element
+ first = true
+
+ list.each do |v|
+ if first
+ first = false
+ elsif sep
+ sep.call
+ else
+ comma_breakable
+ end
+
+ yield(v)
+ end
+ end
+
+ # --------------------------------------------------------------------------
+ # Markers node builders
+ # --------------------------------------------------------------------------
+
+ # This says "you can break a line here if necessary", and a +width+\-column
+ # text +separator+ is inserted if a line is not broken at the point.
+ #
+ # If +separator+ is not specified, ' ' is used.
+ #
+ # If +width+ is not specified, +separator.length+ is used. You will have to
+ # specify this when +separator+ is a multibyte character, for example.
+ #
+ # By default, if the surrounding group is broken and a newline is inserted,
+ # the printer will indent the subsequent line up to the current level of
+ # indentation. You can disable this behavior with the +indent+ argument if
+ # that's not desired (rare).
+ #
+ # By default, when you insert a Breakable into the print tree, it only
+ # breaks the surrounding group when the group's contents cannot fit onto the
+ # remaining space of the current line. You can force it to break the
+ # surrounding group instead if you always want the newline with the +force+
+ # argument.
+ #
+ # There are a few circumstances where you'll want to force the newline into
+ # the output but no insert a break parent (because you don't want to
+ # necessarily force the groups to break unless they need to). In this case
+ # you can pass `force: :skip_break_parent` to this method and it will not
+ # insert a break parent.
+
+ # The vast majority of breakable calls you receive while formatting are a
+ # space in flat mode and a newline in break mode. Since this is so common,
+ # we have a method here to skip past unnecessary calculation.
+ def breakable_space
+ target << BREAKABLE_SPACE
+ end
+
+ # Another very common breakable call you receive while formatting is an
+ # empty string in flat mode and a newline in break mode. Similar to
+ # breakable_space, this is here for avoid unnecessary calculation.
+ def breakable_empty
+ target << BREAKABLE_EMPTY
+ end
+
+ # The final of the very common breakable calls you receive while formatting
+ # is the normal breakable space but with the addition of the break_parent.
+ def breakable_force
+ target << BREAKABLE_FORCE
+ break_parent
+ end
+
+ # This is the same shortcut as breakable_force, except that it doesn't
+ # indent the next line. This is necessary if you're trying to preserve some
+ # custom formatting like a multi-line string.
+ def breakable_return
+ target << BREAKABLE_RETURN
+ break_parent
+ end
+
+ # This inserts a BreakParent node into the print tree which forces the
+ # surrounding and all parent group nodes to break.
+ def break_parent
+ doc = BREAK_PARENT
+ target << doc
+
+ groups.reverse_each do |group|
+ break if group.break
+ group.break!
+ end
+ end
+
+ # A convenience method which is same as follows:
+ #
+ # text(",")
+ # breakable
+ def comma_breakable
+ text(",")
+ breakable_space
+ end
+
+ # This inserts a Trim node into the print tree which, when printed, will
+ # clear all whitespace at the end of the output buffer. This is useful for
+ # the rare case where you need to delete printed indentation and force the
+ # next node to start at the beginning of the line.
+ def trim
+ target << TRIM
+ end
+
+ # --------------------------------------------------------------------------
+ # Container node builders
+ # --------------------------------------------------------------------------
+
+ # Increases left margin after newline with +indent+ for line breaks added in
+ # the block.
+ def align(indent)
+ contents = []
+ doc = Align.new(indent, contents)
+ target << doc
+
+ with_target(contents) { yield }
+ doc
+ end
+
+ # Groups line break hints added in the block. The line break hints are all
+ # to be used or not.
+ #
+ # If +indent+ is specified, the method call is regarded as nested by
+ # align(indent) { ... }.
+ #
+ # If +open_object+ is specified, text(open_object, open_width) is
+ # called before grouping. If +close_object+ is specified,
+ # text(close_object, close_width) is called after grouping.
+ def group
+ contents = []
+ doc = Group.new(contents)
+
+ groups << doc
+ target << doc
+
+ with_target(contents) { yield }
+ groups.pop
+
+ doc
+ end
+
+ # A small DSL-like object used for specifying the alternative contents to be
+ # printed if the surrounding group doesn't break for an IfBreak node.
+ class IfBreakBuilder
+ attr_reader :q, :flat_contents
+
+ def initialize(q, flat_contents)
+ @q = q
+ @flat_contents = flat_contents
+ end
+
+ def if_flat
+ q.with_target(flat_contents) { yield }
+ end
+ end
+
+ # When we already know that groups are broken, we don't actually need to
+ # track the flat versions of the contents. So this builder version is
+ # effectively a no-op, but we need it to maintain the same API. The only
+ # thing this can impact is that if there's a forced break in the flat
+ # contents, then we need to propagate that break up the whole tree.
+ class IfFlatIgnore
+ attr_reader :q
+
+ def initialize(q)
+ @q = q
+ end
+
+ def if_flat
+ contents = []
+ group = Group.new(contents)
+
+ q.with_target(contents) { yield }
+ q.break_parent if group.break
+ end
+ end
+
+ # Inserts an IfBreak node with the contents of the block being added to its
+ # list of nodes that should be printed if the surrounding node breaks. If it
+ # doesn't, then you can specify the contents to be printed with the #if_flat
+ # method used on the return object from this method. For example,
+ #
+ # q.if_break { q.text('do') }.if_flat { q.text('{') }
+ #
+ # In the example above, if the surrounding group is broken it will print
+ # 'do' and if it is not it will print '{'.
+ def if_break
+ break_contents = []
+ flat_contents = []
+
+ doc = IfBreak.new(break_contents, flat_contents)
+ target << doc
+
+ with_target(break_contents) { yield }
+
+ if groups.last.break
+ IfFlatIgnore.new(self)
+ else
+ IfBreakBuilder.new(self, flat_contents)
+ end
+ end
+
+ # This is similar to if_break in that it also inserts an IfBreak node into
+ # the print tree, however it's starting from the flat contents, and cannot
+ # be used to build the break contents.
+ def if_flat
+ if groups.last.break
+ contents = []
+ group = Group.new(contents)
+
+ with_target(contents) { yield }
+ break_parent if group.break
+ else
+ flat_contents = []
+ doc = IfBreak.new(break_contents, flat_contents)
+ target << doc
+
+ with_target(flat_contents) { yield }
+ doc
+ end
+ end
+
+ # Very similar to the #nest method, this indents the nested content by one
+ # level by inserting an Indent node into the print tree. The contents of the
+ # node are determined by the block.
+ def indent
+ contents = []
+ doc = Indent.new(contents)
+ target << doc
+
+ with_target(contents) { yield }
+ doc
+ end
+
+ # Inserts a LineSuffix node into the print tree. The contents of the node
+ # are determined by the block.
+ def line_suffix(priority)
+ contents = []
+ doc = LineSuffix.new(priority, contents)
+ target << doc
+
+ with_target(contents) { yield }
+ doc
+ end
+
+ # Push a value onto the output target.
+ def text(value)
+ target << value
+ end
end
# This is a special node that we insert into the syntax tree to represent the
@@ -3628,8 +4375,7 @@ def format
end
root.accept(formatter)
- formatter.flush
- formatter.buffer.join
+ formatter.format
end
end
end
diff --git a/lib/syntax_tree/prism/prettier_print.rb b/lib/syntax_tree/prism/prettier_print.rb
deleted file mode 100644
index f68bdd3..0000000
--- a/lib/syntax_tree/prism/prettier_print.rb
+++ /dev/null
@@ -1,897 +0,0 @@
-# frozen_string_literal: true
-#
-# This class implements a pretty printing algorithm. It finds line breaks and
-# nice indentations for grouped structure.
-#
-# By default, the class assumes that primitive elements are strings and each
-# byte in the strings is a single column in width. But it can be used for other
-# situations by giving suitable arguments for some methods:
-#
-# * newline object and space generation block for PrettierPrint.new
-# * optional width argument for PrettierPrint#text
-# * PrettierPrint#breakable
-#
-# There are several candidate uses:
-# * text formatting using proportional fonts
-# * multibyte characters which has columns different to number of bytes
-# * non-string formatting
-#
-# == Usage
-#
-# To use this module, you will need to generate a tree of print nodes that
-# represent indentation and newline behavior before it gets sent to the printer.
-# Each node has different semantics, depending on the desired output.
-#
-# The most basic node is a Text node. This represents plain text content that
-# cannot be broken up even if it doesn't fit on one line. You would create one
-# of those with the text method, as in:
-#
-# PrettierPrint.format { |q| q.text('my content') }
-#
-# No matter what the desired output width is, the output for the snippet above
-# will always be the same.
-#
-# If you want to allow the printer to break up the content on the space
-# character when there isn't enough width for the full string on the same line,
-# you can use the Breakable and Group nodes. For example:
-#
-# PrettierPrint.format do |q|
-# q.group do
-# q.text("my")
-# q.breakable
-# q.text("content")
-# end
-# end
-#
-# Now, if everything fits on one line (depending on the maximum width specified)
-# then it will be the same output as the first example. If, however, there is
-# not enough room on the line, then you will get two lines of output, one for
-# the first string and one for the second.
-#
-# There are other nodes for the print tree as well, described in the
-# documentation below. They control alignment, indentation, conditional
-# formatting, and more.
-#
-# == References
-# Christian Lindig, Strictly Pretty, March 2000
-# https://lindig.github.io/papers/strictly-pretty-2000.pdf
-#
-# Philip Wadler, A prettier printer, March 1998
-# https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf
-#
-class PrettierPrint
- # A node in the print tree that represents aligning nested nodes to a certain
- # prefix width or string.
- class Align
- attr_reader :indent, :contents
-
- def initialize(indent, contents)
- @indent = indent
- @contents = contents
- end
-
- def type
- :align
- end
- end
-
- # A node in the print tree that represents a place in the buffer that the
- # content can be broken onto multiple lines.
- class Breakable
- attr_reader :separator, :width
-
- def initialize(
- separator = " ",
- width = separator.length,
- force = false,
- indent = true
- )
- @separator = separator
- @width = width
- @force = force
- @indent = indent
- end
-
- def force?
- @force
- end
-
- def indent?
- @indent
- end
-
- def type
- :breakable
- end
- end
-
- # Below here are the most common combination of options that are created when
- # creating new breakables. They are here to cut down on some allocations.
- BREAKABLE_SPACE = Breakable.new(" ", 1, false, true).freeze
- BREAKABLE_EMPTY = Breakable.new("", 0, false, true).freeze
- BREAKABLE_FORCE = Breakable.new(" ", 1, true, true).freeze
- BREAKABLE_RETURN = Breakable.new(" ", 1, true, false).freeze
-
- # A node in the print tree that forces the surrounding group to print out in
- # the "break" mode as opposed to the "flat" mode. Useful for when you need to
- # force a newline into a group.
- class BreakParent
- def type
- :break_parent
- end
- end
-
- # Since there's really no difference in these instances, just using the same
- # one saves on some allocations.
- BREAK_PARENT = BreakParent.new.freeze
-
- # A node in the print tree that represents a group of items which the printer
- # should try to fit onto one line. This is the basic command to tell the
- # printer when to break. Groups are usually nested, and the printer will try
- # to fit everything on one line, but if it doesn't fit it will break the
- # outermost group first and try again. It will continue breaking groups until
- # everything fits (or there are no more groups to break).
- class Group
- attr_reader :contents
-
- def initialize(contents = [])
- @contents = contents
- @break = false
- end
-
- def break
- @break = true
- end
-
- def break?
- @break
- end
-
- def type
- :group
- end
- end
-
- # A node in the print tree that represents printing one thing if the
- # surrounding group node is broken and another thing if the surrounding group
- # node is flat.
- class IfBreak
- attr_reader :break_contents, :flat_contents
-
- def initialize(break_contents, flat_contents)
- @break_contents = break_contents
- @flat_contents = flat_contents
- end
-
- def type
- :if_break
- end
- end
-
- # A node in the print tree that is a variant of the Align node that indents
- # its contents by one level.
- class Indent
- attr_reader :contents
-
- def initialize(contents)
- @contents = contents
- end
-
- def type
- :indent
- end
- end
-
- # A node in the print tree that has its own special buffer for implementing
- # content that should flush before any newline.
- #
- # Useful for implementating trailing content, as it's not always practical to
- # constantly check where the line ends to avoid accidentally printing some
- # content after a line suffix node.
- class LineSuffix
- DEFAULT_PRIORITY = 1
-
- attr_reader :priority, :contents
-
- def initialize(priority, contents)
- @priority = priority
- @contents = contents
- end
-
- def type
- :line_suffix
- end
- end
-
- # A node in the print tree that represents trimming all of the indentation of
- # the current line, in the rare case that you need to ignore the indentation
- # that you've already created. This node should be placed after a Breakable.
- class Trim
- def type
- :trim
- end
- end
-
- using Module.new {
- refine String do
- def type
- :string
- end
- end
- }
-
- # Since all of the instances here are the same, we can reuse the same one to
- # cut down on allocations.
- TRIM = Trim.new.freeze
-
- # There are two modes in printing, break and flat. When we're in break mode,
- # any lines will use their newline, any if-breaks will use their break
- # contents, etc.
- MODE_BREAK = 1
-
- # This is another print mode much like MODE_BREAK. When we're in flat mode, we
- # attempt to print everything on one line until we either hit a broken group,
- # a forced line, or the maximum width.
- MODE_FLAT = 2
-
- # The default indentation for printing is zero, assuming that the code starts
- # at the top level. That can be changed if desired to start from a different
- # indentation level.
- DEFAULT_INDENTATION = 0
-
- # This is a convenience method which is same as follows:
- #
- # begin
- # q = PrettierPrint.new(output, maxwidth, newline)
- # ...
- # q.flush
- # output
- # end
- #
- def self.format(
- output = "".dup,
- maxwidth = 80,
- indentation = DEFAULT_INDENTATION
- )
- q = new(output, maxwidth)
- yield q
- q.flush(indentation)
- output
- end
-
- # The output object. It represents the final destination of the contents of
- # the print tree. It should respond to <<.
- #
- # This defaults to "".dup
- attr_reader :output
-
- # This is an output buffer that wraps the output object and provides
- # additional functionality depending on its type.
- #
- # This defaults to Buffer::StringBuffer.new("".dup)
- attr_reader :buffer
-
- # The maximum width of a line, before it is separated in to a newline
- #
- # This defaults to 80, and should be an Integer
- attr_reader :maxwidth
-
- # The stack of groups that are being printed.
- attr_reader :groups
-
- # The current array of contents that calls to methods that generate print tree
- # nodes will append to.
- attr_reader :target
-
- # Creates a buffer for pretty printing.
- #
- # +output+ is an output target. If it is not specified, '' is assumed. It
- # should have a << method which accepts the first argument +obj+ of
- # PrettierPrint#text, the first argument +separator+ of PrettierPrint#breakable,
- # and the result of a given block for PrettierPrint.new.
- #
- # +maxwidth+ specifies maximum line length. If it is not specified, 80 is
- # assumed. However actual outputs may overflow +maxwidth+ if long
- # non-breakable texts are provided.
- #
- # The block is used to generate spaces. ->(n) { ' ' * n } is used if it is not
- # given.
- def initialize(buffer = [], maxwidth = 80)
- @buffer = buffer
- @maxwidth = maxwidth
- contents = []
- @groups = [Group.new(contents)]
- @target = contents
- end
-
- # Flushes all of the generated print tree onto the output buffer, then clears
- # the generated tree from memory.
- def flush(base_indentation = DEFAULT_INDENTATION)
- # First, get the root group, since we placed one at the top to begin with.
- doc = groups.first
-
- # This represents how far along the current line we are. It gets reset
- # back to 0 when we encounter a newline.
- position = base_indentation
-
- # Start the buffer with the base indentation level.
- buffer << " " * base_indentation if base_indentation > 0
-
- # This is our command stack. A command consists of a triplet of an
- # indentation level, the mode (break or flat), and a doc node.
- commands = [[base_indentation, MODE_BREAK, doc]]
-
- # This is a small optimization boolean. It keeps track of whether or not
- # when we hit a group node we should check if it fits on the same line.
- should_remeasure = false
-
- # This is a separate command stack that includes the same kind of triplets
- # as the commands variable. It is used to keep track of things that should
- # go at the end of printed lines once the other doc nodes are accounted for.
- # Typically this is used to implement comments.
- line_suffixes = []
-
- # This is a special sort used to order the line suffixes by both the
- # priority set on the line suffix and the index it was in the original
- # array.
- line_suffix_sort = ->(line_suffix) do
- [-line_suffix.last.priority, -line_suffixes.index(line_suffix)]
- end
-
- # This is a linear stack instead of a mutually recursive call defined on
- # the individual doc nodes for efficiency.
- while (indent, mode, doc = commands.pop)
- case doc.type
- when :string
- buffer << doc
- position += doc.length
- when :group
- if mode == MODE_FLAT && !should_remeasure
- next_mode = doc.break? ? MODE_BREAK : MODE_FLAT
- commands += doc.contents.reverse.map { |part| [indent, next_mode, part] }
- else
- should_remeasure = false
-
- if doc.break?
- commands += doc.contents.reverse.map { |part| [indent, MODE_BREAK, part] }
- else
- next_commands = doc.contents.reverse.map { |part| [indent, MODE_FLAT, part] }
-
- if fits?(next_commands, commands, maxwidth - position)
- commands += next_commands
- else
- commands += next_commands.map { |command| command[1] = MODE_BREAK; command }
- end
- end
- end
- when :breakable
- if mode == MODE_FLAT
- if doc.force?
- # This line was forced into the output even if we were in flat mode,
- # so we need to tell the next group that no matter what, it needs to
- # remeasure because the previous measurement didn't accurately
- # capture the entire expression (this is necessary for nested
- # groups).
- should_remeasure = true
- else
- buffer << doc.separator
- position += doc.width
- next
- end
- end
-
- # If there are any commands in the line suffix buffer, then we're going
- # to flush them now, as we are about to add a newline.
- if line_suffixes.any?
- commands << [indent, mode, doc]
-
- line_suffixes.sort_by(&line_suffix_sort).each do |(indent, mode, doc)|
- commands += doc.contents.reverse.map { |part| [indent, mode, part] }
- end
-
- line_suffixes.clear
- next
- end
-
- if !doc.indent?
- buffer << "\n"
- position = 0
- else
- position -= trim!(buffer)
- buffer << "\n"
- buffer << " " * indent
- position = indent
- end
- when :indent
- next_indent = indent + 2
- commands += doc.contents.reverse.map { |part| [next_indent, mode, part] }
- when :align
- next_indent = indent + doc.indent
- commands += doc.contents.reverse.map { |part| [next_indent, mode, part] }
- when :trim
- position -= trim!(buffer)
- when :if_break
- if mode == MODE_BREAK && doc.break_contents.any?
- commands += doc.break_contents.reverse.map { |part| [indent, mode, part] }
- elsif mode == MODE_FLAT && doc.flat_contents.any?
- commands += doc.flat_contents.reverse.map { |part| [indent, mode, part] }
- end
- when :line_suffix
- line_suffixes << [indent, mode, doc]
- when :break_parent
- # do nothing
- else
- # Special case where the user has defined some way to get an extra doc
- # node that we don't explicitly support into the list. In this case
- # we're going to assume it's 0-width and just append it to the output
- # buffer.
- #
- # This is useful behavior for putting marker nodes into the list so that
- # you can know how things are getting mapped before they get printed.
- buffer << doc
- end
-
- if commands.empty? && line_suffixes.any?
- line_suffixes.sort_by(&line_suffix_sort).each do |(indent, mode, doc)|
- commands.concat doc.contents.reverse.map { |part| [indent, mode, part] }
- end
-
- line_suffixes.clear
- end
- end
- end
-
- # ----------------------------------------------------------------------------
- # Helper node builders
- # ----------------------------------------------------------------------------
-
- # The vast majority of breakable calls you receive while formatting are a
- # space in flat mode and a newline in break mode. Since this is so common,
- # we have a method here to skip past unnecessary calculation.
- def breakable_space
- target << BREAKABLE_SPACE
- end
-
- # Another very common breakable call you receive while formatting is an
- # empty string in flat mode and a newline in break mode. Similar to
- # breakable_space, this is here for avoid unnecessary calculation.
- def breakable_empty
- target << BREAKABLE_EMPTY
- end
-
- # The final of the very common breakable calls you receive while formatting
- # is the normal breakable space but with the addition of the break_parent.
- def breakable_force
- target << BREAKABLE_FORCE
- break_parent
- end
-
- # This is the same shortcut as breakable_force, except that it doesn't indent
- # the next line. This is necessary if you're trying to preserve some custom
- # formatting like a multi-line string.
- def breakable_return
- target << BREAKABLE_RETURN
- break_parent
- end
-
- # A convenience method which is same as follows:
- #
- # text(",")
- # breakable
- def comma_breakable
- text(",")
- breakable_space
- end
-
- # This is similar to #breakable except the decision to break or not is
- # determined individually.
- #
- # Two #fill_breakable under a group may cause 4 results:
- # (break,break), (break,non-break), (non-break,break), (non-break,non-break).
- # This is different to #breakable because two #breakable under a group
- # may cause 2 results: (break,break), (non-break,non-break).
- #
- # The text +separator+ is inserted if a line is not broken at this point.
- #
- # If +separator+ is not specified, ' ' is used.
- #
- # If +width+ is not specified, +separator.length+ is used. You will have to
- # specify this when +separator+ is a multibyte character, for example.
- def fill_breakable(separator = " ", width = separator.length)
- group { breakable(separator, width) }
- end
-
- # This method calculates the position of the text relative to the current
- # indentation level when the doc has been printed. It's useful for
- # determining how to align text to doc nodes that are already built into the
- # tree.
- def last_position(node)
- queue = [node]
- width = 0
-
- while (doc = queue.shift)
- case doc.type
- when :string
- width += doc.length
- when :group, :indent, :align
- queue = doc.contents + queue
- when :breakable
- width = 0
- when :if_break
- queue = doc.break_contents + queue
- end
- end
-
- width
- end
-
- # This method will remove any breakables from the list of contents so that
- # no newlines are present in the output. If a newline is being forced into
- # the output, the replace value will be used.
- def remove_breaks(node, replace = "; ")
- queue = [node]
-
- while (doc = queue.shift)
- case doc.type
- when :align, :indent, :group
- doc.contents.map! { |child| remove_breaks_with(child, replace) }
- queue += doc.contents
- when :if_break
- doc.flat_contents.map! { |child| remove_breaks_with(child, replace) }
- queue += doc.flat_contents
- end
- end
- end
-
- # Adds a separated list.
- # The list is separated by comma with breakable space, by default.
- #
- # #seplist iterates the +list+ using +each+.
- # It yields each object to the block given for #seplist.
- # The procedure +separator_proc+ is called between each yields.
- #
- # If the iteration is zero times, +separator_proc+ is not called at all.
- #
- # If +separator_proc+ is nil or not given,
- # +lambda { comma_breakable }+ is used.
- #
- # For example, following 3 code fragments has similar effect.
- #
- # q.seplist([1,2,3]) {|v| xxx v }
- #
- # q.seplist([1,2,3], lambda { q.comma_breakable }, :each) {|v| xxx v }
- #
- # xxx 1
- # q.comma_breakable
- # xxx 2
- # q.comma_breakable
- # xxx 3
- def seplist(list, sep = nil) # :yield: element
- first = true
-
- list.each do |v|
- if first
- first = false
- elsif sep
- sep.call
- else
- comma_breakable
- end
-
- yield(v)
- end
- end
-
- # ----------------------------------------------------------------------------
- # Markers node builders
- # ----------------------------------------------------------------------------
-
- # This says "you can break a line here if necessary", and a +width+\-column
- # text +separator+ is inserted if a line is not broken at the point.
- #
- # If +separator+ is not specified, ' ' is used.
- #
- # If +width+ is not specified, +separator.length+ is used. You will have to
- # specify this when +separator+ is a multibyte character, for example.
- #
- # By default, if the surrounding group is broken and a newline is inserted,
- # the printer will indent the subsequent line up to the current level of
- # indentation. You can disable this behavior with the +indent+ argument if
- # that's not desired (rare).
- #
- # By default, when you insert a Breakable into the print tree, it only breaks
- # the surrounding group when the group's contents cannot fit onto the
- # remaining space of the current line. You can force it to break the
- # surrounding group instead if you always want the newline with the +force+
- # argument.
- #
- # There are a few circumstances where you'll want to force the newline into
- # the output but no insert a break parent (because you don't want to
- # necessarily force the groups to break unless they need to). In this case you
- # can pass `force: :skip_break_parent` to this method and it will not insert
- # a break parent.`
- def breakable(
- separator = " ",
- width = separator.length,
- indent: true,
- force: false
- )
- target << Breakable.new(separator, width, !!force, indent)
- break_parent if force == true
- end
-
- # This inserts a BreakParent node into the print tree which forces the
- # surrounding and all parent group nodes to break.
- def break_parent
- doc = BREAK_PARENT
- target << doc
-
- groups.reverse_each do |group|
- break if group.break?
- group.break
- end
- end
-
- # This inserts a Trim node into the print tree which, when printed, will clear
- # all whitespace at the end of the output buffer. This is useful for the rare
- # case where you need to delete printed indentation and force the next node
- # to start at the beginning of the line.
- def trim
- target << TRIM
- end
-
- # ----------------------------------------------------------------------------
- # Container node builders
- # ----------------------------------------------------------------------------
-
- # Groups line break hints added in the block. The line break hints are all to
- # be used or not.
- #
- # If +indent+ is specified, the method call is regarded as nested by
- # nest(indent) { ... }.
- #
- # If +open_object+ is specified, text(open_object, open_width) is
- # called before grouping. If +close_object+ is specified,
- # text(close_object, close_width) is called after grouping.
- def group
- contents = []
- doc = Group.new(contents)
-
- groups << doc
- target << doc
-
- with_target(contents) { yield }
- groups.pop
-
- doc
- end
-
- # A small DSL-like object used for specifying the alternative contents to be
- # printed if the surrounding group doesn't break for an IfBreak node.
- class IfBreakBuilder
- attr_reader :q, :flat_contents
-
- def initialize(q, flat_contents)
- @q = q
- @flat_contents = flat_contents
- end
-
- def if_flat
- q.with_target(flat_contents) { yield }
- end
- end
-
- # When we already know that groups are broken, we don't actually need to track
- # the flat versions of the contents. So this builder version is effectively a
- # no-op, but we need it to maintain the same API. The only thing this can
- # impact is that if there's a forced break in the flat contents, then we need
- # to propagate that break up the whole tree.
- class IfFlatIgnore
- attr_reader :q
-
- def initialize(q)
- @q = q
- end
-
- def if_flat
- contents = []
- group = Group.new(contents)
-
- q.with_target(contents) { yield }
- q.break_parent if group.break?
- end
- end
-
- # Inserts an IfBreak node with the contents of the block being added to its
- # list of nodes that should be printed if the surrounding node breaks. If it
- # doesn't, then you can specify the contents to be printed with the #if_flat
- # method used on the return object from this method. For example,
- #
- # q.if_break { q.text('do') }.if_flat { q.text('{') }
- #
- # In the example above, if the surrounding group is broken it will print 'do'
- # and if it is not it will print '{'.
- def if_break
- break_contents = []
- flat_contents = []
-
- doc = IfBreak.new(break_contents, flat_contents)
- target << doc
-
- with_target(break_contents) { yield }
-
- if groups.last.break?
- IfFlatIgnore.new(self)
- else
- IfBreakBuilder.new(self, flat_contents)
- end
- end
-
- # This is similar to if_break in that it also inserts an IfBreak node into the
- # print tree, however it's starting from the flat contents, and cannot be used
- # to build the break contents.
- def if_flat
- if groups.last.break?
- contents = []
- group = Group.new(contents)
-
- with_target(contents) { yield }
- break_parent if group.break?
- else
- flat_contents = []
- doc = IfBreak.new(break_contents, flat_contents)
- target << doc
-
- with_target(flat_contents) { yield }
- doc
- end
- end
-
- # Very similar to the #nest method, this indents the nested content by one
- # level by inserting an Indent node into the print tree. The contents of the
- # node are determined by the block.
- def indent
- contents = []
- doc = Indent.new(contents)
- target << doc
-
- with_target(contents) { yield }
- doc
- end
-
- # Inserts a LineSuffix node into the print tree. The contents of the node are
- # determined by the block.
- def line_suffix(priority)
- contents = []
- doc = LineSuffix.new(priority, contents)
- target << doc
-
- with_target(contents) { yield }
- doc
- end
-
- # Increases left margin after newline with +indent+ for line breaks added in
- # the block.
- def nest(indent)
- contents = []
- doc = Align.new(indent, contents)
- target << doc
-
- with_target(contents) { yield }
- doc
- end
-
- # Push a value onto the output target.
- def text(value)
- target << value
- end
-
- # ----------------------------------------------------------------------------
- # Internal APIs
- # ----------------------------------------------------------------------------
-
- # A convenience method used by a lot of the print tree node builders that
- # temporarily changes the target that the builders will append to.
- def with_target(target)
- previous_target, @target = @target, target
- yield
- @target = previous_target
- end
-
- private
-
- def trim!(buffer)
- return 0 if buffer.empty?
-
- trimmed = 0
-
- while buffer.any? && buffer.last.is_a?(String) &&
- buffer.last.match?(/\A[\t ]*\z/)
- trimmed += buffer.pop.length
- end
-
- if buffer.any? && buffer.last.is_a?(String) && !buffer.last.frozen?
- length = buffer.last.length
- buffer.last.gsub!(/[\t ]*\z/, "")
- trimmed += length - buffer.last.length
- end
-
- trimmed
- end
-
- # This method returns a boolean as to whether or not the remaining commands
- # fit onto the remaining space on the current line. If we finish printing
- # all of the commands or if we hit a newline, then we return true. Otherwise
- # if we continue printing past the remaining space, we return false.
- def fits?(next_commands, rest_commands, remaining)
- # This is the index in the remaining commands that we've handled so far.
- # We reverse through the commands and add them to the stack if we've run
- # out of nodes to handle.
- rest_index = rest_commands.length
-
- # This is our stack of commands, very similar to the commands list in the
- # print method.
- commands = [*next_commands]
-
- # This is our output buffer, really only necessary to keep track of
- # because we could encounter a Trim doc node that would actually add
- # remaining space.
- fit_buffer = []
-
- while remaining >= 0
- if commands.empty?
- return true if rest_index == 0
-
- rest_index -= 1
- commands << rest_commands[rest_index]
- next
- end
-
- indent, mode, doc = commands.pop
-
- case doc.type
- when :string
- fit_buffer << doc
- remaining -= doc.length
- when :group
- next_mode = doc.break? ? MODE_BREAK : mode
- commands += doc.contents.reverse.map { |part| [indent, next_mode, part] }
- when :breakable
- if mode == MODE_FLAT && !doc.force?
- fit_buffer << doc.separator
- remaining -= doc.width
- next
- end
-
- return true
- when :indent
- next_indent = indent + 2
- commands += doc.contents.reverse.map { |part| [next_indent, mode, part] }
- when :align
- next_indent = indent + doc.indent
- commands += doc.contents.reverse.map { |part| [next_indent, mode, part] }
- when :trim
- remaining += trim!(fit_buffer)
- when :if_break
- if mode == MODE_BREAK && doc.break_contents.any?
- commands += doc.break_contents.reverse.map { |part| [indent, mode, part] }
- elsif mode == MODE_FLAT && doc.flat_contents.any?
- commands += doc.flat_contents.reverse.map { |part| [indent, mode, part] }
- end
- end
- end
-
- false
- end
-
- def remove_breaks_with(doc, replace)
- case doc.type
- when :breakable
- doc.force? ? replace : doc.separator
- when :if_break
- Align.new(0, doc.flat_contents)
- else
- doc
- end
- end
-end