Class: PrettierPrint

Inherits:
Object
  • Object
show all
Defined in:
lib/prettier_print.rb,
lib/prettier_print/version.rb,
lib/prettier_print/single_line.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).freeze
BREAKABLE_EMPTY =
Breakable.new("", 0, indent: true, force: false).freeze
BREAKABLE_FORCE =
Breakable.new(" ", 1, indent: true, force: true).freeze
BREAKABLE_RETURN =
Breakable.new(" ", 1, indent: false, force: true).freeze
BREAK_PARENT =

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

BreakParent.new.freeze
TRIM =

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

Trim.new.freeze
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
DEFAULT_INDENTATION =

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.

0
VERSION =
"1.2.1"

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.



441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/prettier_print.rb', line 441

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)



400
401
402
# File 'lib/prettier_print.rb', line 400

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 }



416
417
418
# File 'lib/prettier_print.rb', line 416

def genspace
  @genspace
end

#groupsObject (readonly)

The stack of groups that are being printed.



419
420
421
# File 'lib/prettier_print.rb', line 419

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



405
406
407
# File 'lib/prettier_print.rb', line 405

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



410
411
412
# File 'lib/prettier_print.rb', line 410

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



394
395
396
# File 'lib/prettier_print.rb', line 394

def output
  @output
end

#targetObject (readonly)

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



423
424
425
# File 'lib/prettier_print.rb', line 423

def target
  @target
end

Class Method Details

.format(output = "".dup, maxwidth = 80, newline = DEFAULT_NEWLINE, genspace = DEFAULT_GENSPACE, indentation = DEFAULT_INDENTATION) {|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)


377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/prettier_print.rb', line 377

def self.format(
  output = "".dup,
  maxwidth = 80,
  newline = DEFAULT_NEWLINE,
  genspace = DEFAULT_GENSPACE,
  indentation = DEFAULT_INDENTATION
)
  q = new(output, maxwidth, newline, &genspace)
  yield q
  q.flush(indentation)
  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)


156
157
158
159
160
161
162
163
164
165
# File 'lib/prettier_print/single_line.rb', line 156

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.



814
815
816
817
818
819
820
821
822
# File 'lib/prettier_print.rb', line 814

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



802
803
804
805
806
807
808
809
810
# File 'lib/prettier_print.rb', line 802

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.



646
647
648
# File 'lib/prettier_print.rb', line 646

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.



652
653
654
655
# File 'lib/prettier_print.rb', line 652

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.



660
661
662
663
# File 'lib/prettier_print.rb', line 660

def breakable_return
  target << BREAKABLE_RETURN
  break_parent
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.



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

def breakable_space
  target << BREAKABLE_SPACE
end

#comma_breakableObject

A convenience method which is same as follows:

text(",")
breakable


669
670
671
672
# File 'lib/prettier_print.rb', line 669

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>


484
485
486
# File 'lib/prettier_print.rb', line 484

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.



688
689
690
# File 'lib/prettier_print.rb', line 688

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

#flush(base_indentation = DEFAULT_INDENTATION) ⇒ Object

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



490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'lib/prettier_print.rb', line 490

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 << genspace.call(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
    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?
      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
    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.



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

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



917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
# File 'lib/prettier_print.rb', line 917

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.



936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
# File 'lib/prettier_print.rb', line 936

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.



956
957
958
959
960
961
962
963
# File 'lib/prettier_print.rb', line 956

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.



696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
# File 'lib/prettier_print.rb', line 696

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.



967
968
969
970
971
972
973
# File 'lib/prettier_print.rb', line 967

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.



977
978
979
980
981
982
983
984
# File 'lib/prettier_print.rb', line 977

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.



721
722
723
724
725
726
727
728
729
730
731
732
733
734
# File 'lib/prettier_print.rb', line 721

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


760
761
762
763
764
765
766
767
768
769
770
771
772
# File 'lib/prettier_print.rb', line 760

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.



989
990
991
992
993
994
995
996
997
998
999
# File 'lib/prettier_print.rb', line 989

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.



828
829
830
# File 'lib/prettier_print.rb', line 828

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.



1007
1008
1009
1010
1011
# File 'lib/prettier_print.rb', line 1007

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