smart plaintext wrapping

Very often you need to break plaintext at a specific width while retaining readability. However, it is easy to get caught up in fencepost errors and similar irritating issues. I wrote the particular implementation here to help display the cheat files on Cheat. I have no doubt that this has been done many times before, but my method does do a few neat things.

features

Indents are preserved.

Linebreaks never occur within words unless the word is longer than the maximum width.

You can set an additional hanging indent so that wrapped lines clearly belong to their parent.

Also optionally, you can have it hang wrapped lines in a list underneath their parent text instead of underneath the list item delimiter.

usage examples

input text
class Employeed < Status
  belongs_to :employee, :class_name => "Person", :foreign_key => 'employee_id'
end

1. To find out what class an instance of the superclass use @instance.class or you can use the value of the type column from other systems that do not go through AR.
2. Fixtures for a STI model must go in the fixture file for the superclass.

Straightforward wrap (notice how the belongs_to line preserves its indent):

chloe:~/Projects/misc/misc eweaver$ ./wrap.rb text 50 0 false
class Employeed < Status
  belongs_to :employee, :class_name => "Person",
  :foreign_key => 'employee_id'
end

1. To find out what class an instance of the
superclass use @instance.class or you can use the
value of the type column from other systems that
do not go through AR.
2. Fixtures for a STI model must go in the fixture
file for the superclass.

How about with some hanging indents on the list, too?

chloe:~/Projects/misc/misc eweaver$ ./wrap.rb text 50 0 true
class Employeed < Status
  belongs_to :employee, :class_name => "Person",
  :foreign_key => 'employee_id'
end

1. To find out what class an instance of the
   superclass use @instance.class or you can use
   the value of the type column from other systems
   that do not go through AR.
2. Fixtures for a STI model must go in the fixture
   file for the superclass.

Etc.

code

wrap.rb
#!/opt/local/bin/ruby

class String

  def wrap(width, hanging_indent = 0, magic_lists = false)
    lines = self.split(/\n/)

    lines.collect! do |line|

      if magic_lists
        line =~ /^([\s\-\d\.\:]*\s)/
      else
        line =~ /^([\s]*\s)/
      end

      indent = $1.length + hanging_indent rescue hanging_indent

      buffer = ""
      first = true

      while line.length > 0
        first ? (i, first = 0, false) : i = indent
        pos = width - i

        if line.length > pos and line[0..pos] =~ /^(.+)\s/
          subline = $1
        else
          subline = line[0..pos]
        end
        buffer += " " * i + subline + "\n"
        line.tail!(subline.length)
      end
      buffer[0..-2]
    end

    lines.join("\n")

  end

  def tail!(pos)
    self[0..pos] = ""
    strip!
  end

end

if __FILE__ == $0
  File.open(ARGV[0]) do |f|
    puts f.read.wrap(ARGV[1].to_i, ARGV[2].to_i, ARGV[3] == "true")
  end
end

It’s really just a few regular expressions and some string munging; that’s all.

3 responses

  1. That’s some good stuff. Very good on the hanging indent stuff! I will definitely use it for the cheat libraries I am writing

  2. I think there is small bug in the code.. Try having a line with a single word of more than wrap characters: in this case, the last character of the first line gets truncated.

    This can be fixed by having

    line.tail!(subline.length)

    changed to

    line.tail!(subline.length-1)