Class: PrettierPrint

Inherits:
Object
  • Object
show all
Defined in:
lib/prettier_print.rb,
lib/prettier_print/version.rb

Overview

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 lindig.github.io/papers/strictly-pretty-2000.pdf

Philip Wadler, A prettier printer, March 1998 homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf

Defined Under Namespace

Modules: Buffer Classes: Align, BreakParent, Breakable, Group, IfBreak, IfBreakBuilder, IfFlatIgnore, Indent, LineSuffix, SingleLine, Text, Trim

Constant Summary collapse

BREAKABLE_SPACE =

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.new(" ", 1, indent: true, force: false)
BREAKABLE_EMPTY =
Breakable.new("", 0, indent: true, force: false)
BREAKABLE_FORCE =
Breakable.new(" ", 1, indent: true, force: true)
BREAKABLE_RETURN =
Breakable.new(" ", 1, indent: false, force: true)
BREAK_PARENT =

Since there’s really no difference in these instances, just using the same one saves on some allocations.

BreakParent.new
TRIM =

Since all of the instances here are the same, we can reuse the same one to cut down on allocations.

Trim.new
DEFAULT_NEWLINE =

When printing, you can optionally specify the value that should be used whenever a group needs to be broken onto multiple lines. In this case the default is n.

"\n"
DEFAULT_GENSPACE =

When generating spaces after a newline for indentation, by default we generate one space per character needed for indentation. You can change this behavior (for instance to use tabs) by passing a different genspace procedure.

->(n) { " " * n }
MODE_BREAK =

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.

1
MODE_FLAT =

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.

2
VERSION =
"1.0.0"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(output = "".dup, maxwidth = 80, newline = DEFAULT_NEWLINE, &genspace) ⇒ PrettierPrint

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, the first argument newline of PrettierPrint.new, 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.

newline is used for line breaks. “n” is used if it is not specified.

The block is used to generate spaces. ->(n) { ‘ ’ * n } is used if it is not given.



597
598
599
600
601
602
603
604
605
606
607
608
609
# File 'lib/prettier_print.rb', line 597

def initialize(
  output = "".dup,
  maxwidth = 80,
  newline = DEFAULT_NEWLINE,
  &genspace
)
  @output = output
  @buffer = Buffer.for(output)
  @maxwidth = maxwidth
  @newline = newline
  @genspace = genspace || DEFAULT_GENSPACE
  reset
end

Instance Attribute Details

#bufferObject (readonly)

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)



556
557
558
# File 'lib/prettier_print.rb', line 556

def buffer
  @buffer
end

#genspaceObject (readonly)

An object that responds to call that takes one argument, of an Integer, and returns the corresponding number of spaces.

By default this is: ->(n) { ‘ ’ * n }



572
573
574
# File 'lib/prettier_print.rb', line 572

def genspace
  @genspace
end

#groupsObject (readonly)

The stack of groups that are being printed.



575
576
577
# File 'lib/prettier_print.rb', line 575

def groups
  @groups
end

#maxwidthObject (readonly)

The maximum width of a line, before it is separated in to a newline

This defaults to 80, and should be an Integer



561
562
563
# File 'lib/prettier_print.rb', line 561

def maxwidth
  @maxwidth
end

#newlineObject (readonly)

The value that is appended to output to add a new line.

This defaults to “n”, and should be String



566
567
568
# File 'lib/prettier_print.rb', line 566

def newline
  @newline
end

#outputObject (readonly)

The output object. It represents the final destination of the contents of the print tree. It should respond to <<.

This defaults to “”.dup



550
551
552
# File 'lib/prettier_print.rb', line 550

def output
  @output
end

#targetObject (readonly)

The current array of contents that calls to methods that generate print tree nodes will append to.



579
580
581
# File 'lib/prettier_print.rb', line 579

def target
  @target
end

Class Method Details

.format(output = "".dup, maxwidth = 80, newline = DEFAULT_NEWLINE, genspace = DEFAULT_GENSPACE) {|q| ... } ⇒ Object

This is a convenience method which is same as follows:

begin
  q = PrettierPrint.new(output, maxwidth, newline, &genspace)
  ...
  q.flush
  output
end

Yields:

  • (q)


516
517
518
519
520
521
522
523
524
525
526
# File 'lib/prettier_print.rb', line 516

def self.format(
  output = "".dup,
  maxwidth = 80,
  newline = DEFAULT_NEWLINE,
  genspace = DEFAULT_GENSPACE
)
  q = new(output, maxwidth, newline, &genspace)
  yield q
  q.flush
  output
end

.singleline_format(output = +"",, _maxwidth = nil, _newline = nil, _genspace = nil) {|q| ... } ⇒ Object

This is similar to PrettierPrint::format but the result has no breaks.

maxwidth, newline and genspace are ignored.

The invocation of breakable in the block doesn’t break a line and is treated as just an invocation of text.

Yields:

  • (q)


535
536
537
538
539
540
541
542
543
544
# File 'lib/prettier_print.rb', line 535

def self.singleline_format(
  output = +"",
  _maxwidth = nil,
  _newline = nil,
  _genspace = nil
)
  q = SingleLine.new(output)
  yield q
  output
end

Instance Method Details

#break_parentObject

This inserts a BreakParent node into the print tree which forces the surrounding and all parent group nodes to break.



963
964
965
966
967
968
969
970
971
# File 'lib/prettier_print.rb', line 963

def break_parent
  doc = BREAK_PARENT
  target << doc

  groups.reverse_each do |group|
    break if group.break?
    group.break
  end
end

#breakable(separator = " ", width = separator.length, indent: true, force: false) ⇒ Object

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.`



951
952
953
954
955
956
957
958
959
# File 'lib/prettier_print.rb', line 951

def breakable(
  separator = " ",
  width = separator.length,
  indent: true,
  force: false
)
  target << Breakable.new(separator, width, indent: indent, force: !!force)
  break_parent if force == true
end

#breakable_emptyObject

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.



796
797
798
# File 'lib/prettier_print.rb', line 796

def breakable_empty
  target << BREAKABLE_EMPTY
end

#breakable_forceObject

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.



802
803
804
805
# File 'lib/prettier_print.rb', line 802

def breakable_force
  target << BREAKABLE_FORCE
  break_parent
end

#breakable_returnObject

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.



810
811
812
# File 'lib/prettier_print.rb', line 810

def breakable_return
  target << BREAKABLE_RETURN
end

#breakable_spaceObject

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.



789
790
791
# File 'lib/prettier_print.rb', line 789

def breakable_space
  target << BREAKABLE_SPACE
end

#comma_breakableObject

A convenience method which is same as follows:

text(",")
breakable


818
819
820
821
# File 'lib/prettier_print.rb', line 818

def comma_breakable
  text(",")
  breakable_space
end

#current_groupObject

Returns the group most recently added to the stack.

Contrived example:

out = ""
=> ""
q = PrettierPrint.new(out)
=> #<PrettierPrint:0x0>
q.group {
  q.text q.current_group.inspect
  q.text q.newline
  q.group(q.current_group.depth + 1) {
    q.text q.current_group.inspect
    q.text q.newline
    q.group(q.current_group.depth + 1) {
      q.text q.current_group.inspect
      q.text q.newline
      q.group(q.current_group.depth + 1) {
        q.text q.current_group.inspect
        q.text q.newline
      }
    }
  }
}
=> 284
 puts out
#<PrettierPrint::Group:0x0 @depth=1>
#<PrettierPrint::Group:0x0 @depth=2>
#<PrettierPrint::Group:0x0 @depth=3>
#<PrettierPrint::Group:0x0 @depth=4>


640
641
642
# File 'lib/prettier_print.rb', line 640

def current_group
  groups.last
end

#fill_breakable(separator = " ", width = separator.length) ⇒ Object

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.



837
838
839
# File 'lib/prettier_print.rb', line 837

def fill_breakable(separator = " ", width = separator.length)
  group { breakable(separator, width) }
end

#flushObject

Flushes all of the generated print tree onto the output buffer, then clears the generated tree from memory.



646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
# File 'lib/prettier_print.rb', line 646

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
    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 << newline
        position = 0
      else
        position -= buffer.trim!
        buffer << newline
        buffer << genspace.call(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 -= buffer.trim!
    when IfBreak
      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 LineSuffix
      line_suffixes << [indent, mode, doc]
    when BreakParent
      # do nothing
    when Text
      doc.objects.each { |object| buffer << object }
      position += doc.width
    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?
      commands += line_suffixes.sort_by(&line_suffix_sort)
      line_suffixes = []
    end
  end

  # Reset the group stack and target array so that this pretty printer object
  # can continue to be used before calling flush again if desired.
  reset
end

#group(indent = 0, open_object = "", close_object = "", open_width = open_object.length, close_width = close_object.length) ⇒ Object

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.



994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
# File 'lib/prettier_print.rb', line 994

def group(
  indent = 0,
  open_object = "",
  close_object = "",
  open_width = open_object.length,
  close_width = close_object.length
)
  text(open_object, open_width) if open_object != ""

  doc = Group.new(groups.last.depth + 1)
  groups << doc
  target << doc

  with_target(doc.contents) do
    if indent != 0
      nest(indent) { yield }
    else
      yield
    end
  end

  groups.pop
  text(close_object, close_width) if close_object != ""

  doc
end

#if_breakObject

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 ‘{’.



1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
# File 'lib/prettier_print.rb', line 1066

def if_break
  break_contents = []
  flat_contents = []

  doc = IfBreak.new(break_contents: break_contents, flat_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

#if_flatObject

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.



1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
# File 'lib/prettier_print.rb', line 1085

def if_flat
  if groups.last.break?
    contents = []
    group = Group.new(0, contents: contents)

    with_target(contents) { yield }
    break_parent if group.break?
  else
    flat_contents = []
    doc = IfBreak.new(break_contents: [], flat_contents: flat_contents)
    target << doc

    with_target(flat_contents) { yield }
    doc
  end
end

#indentObject

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.



1105
1106
1107
1108
1109
1110
1111
1112
# File 'lib/prettier_print.rb', line 1105

def indent
  contents = []
  doc = Indent.new(contents: contents)
  target << doc

  with_target(contents) { yield }
  doc
end

#last_position(node) ⇒ Object

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.



845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
# File 'lib/prettier_print.rb', line 845

def last_position(node)
  queue = [node]
  width = 0

  while (doc = queue.shift)
    case doc
    when String
      width += doc.length
    when Group, Indent, Align
      queue = doc.contents + queue
    when Breakable
      width = 0
    when IfBreak
      queue = doc.break_contents + queue
    when Text
      width += doc.width
    end
  end

  width
end

#line_suffix(priority: LineSuffix::DEFAULT_PRIORITY) ⇒ Object

Inserts a LineSuffix node into the print tree. The contents of the node are determined by the block.



1116
1117
1118
1119
1120
1121
1122
# File 'lib/prettier_print.rb', line 1116

def line_suffix(priority: LineSuffix::DEFAULT_PRIORITY)
  doc = LineSuffix.new(priority: priority)
  target << doc

  with_target(doc.contents) { yield }
  doc
end

#nest(indent) ⇒ Object

Increases left margin after newline with indent for line breaks added in the block.



1126
1127
1128
1129
1130
1131
1132
1133
# File 'lib/prettier_print.rb', line 1126

def nest(indent)
  contents = []
  doc = Align.new(indent: indent, contents: contents)
  target << doc

  with_target(contents) { yield }
  doc
end

#remove_breaks(node, replace = "; ") ⇒ Object

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.



870
871
872
873
874
875
876
877
878
879
880
881
882
883
# File 'lib/prettier_print.rb', line 870

def remove_breaks(node, replace = "; ")
  queue = [node]

  while (doc = queue.shift)
    case doc
    when Align, Indent, Group
      doc.contents.map! { |child| remove_breaks_with(child, replace) }
      queue += doc.contents
    when IfBreak
      doc.flat_contents.map! { |child| remove_breaks_with(child, replace) }
      queue += doc.flat_contents
    end
  end
end

#seplist(list, sep = nil, iter_method = :each) ⇒ Object

Adds a separated list. The list is separated by comma with breakable space, by default.

#seplist iterates the list using iter_method. 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. If iter_method is not given, :each 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


909
910
911
912
913
914
915
916
917
918
919
920
921
# File 'lib/prettier_print.rb', line 909

def seplist(list, sep=nil, iter_method=:each) # :yield: element
  first = true
  list.__send__(iter_method) {|*v|
    if first
      first = false
    elsif sep
      sep.call
    else
      comma_breakable
    end
    RUBY_VERSION >= "3.0" ? yield(*v, **{}) : yield(*v)
  }
end

#text(object = "", width = object.length) ⇒ Object

This adds object as a text of width columns in width.

If width is not specified, object.length is used.



1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
# File 'lib/prettier_print.rb', line 1138

def text(object = "", width = object.length)
  doc = target.last

  unless doc.is_a?(Text)
    doc = Text.new
    target << doc
  end

  doc.add(object: object, width: width)
  doc
end

#trimObject

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.



977
978
979
# File 'lib/prettier_print.rb', line 977

def trim
  target << TRIM
end

#with_target(target) ⇒ Object

A convenience method used by a lot of the print tree node builders that temporarily changes the target that the builders will append to.



1156
1157
1158
1159
1160
# File 'lib/prettier_print.rb', line 1156

def with_target(target)
  previous_target, @target = @target, target
  yield
  @target = previous_target
end