IndentedIO

IndentedIO extends Kernel, IO, and StringIO with an #indent method that returns an IndentedIO object. The IndentedIO object acts as the original object but redefines #print, #printf, #puts, and #p to print their output indented. Indentations are stacked so that each new indentation adds to the previous indendation

Usage

require 'indented_io'

puts 'Not indented'
indent { puts 'Indented one level' }
indent(2, '* ').puts 'Indented two levels'

outputs

Not indented
  Indented one level
* * Indented two levels

Kernel#indent, IO#indent, and StringIO#indent

#indent without a block returns an IndentedIO object that acts as the receiver but redefine #print, #printf, #puts, and #p to print indented If given a block, the block will be called with the IndentedIO object as argument:

$stdout.puts 'Not indented'
$stdout.indent.puts 'Indented'
$stdout.indent { |f| f.puts 'Indented' }

# Not indented
#   Indented
#   Indented

(please note that when Kernel is the receiver, the returned object will act as the $stdout object and not the Kernel object)

#indent can take up to two positional arguments: level, that is the number of levels to indent (default 1), and the indent string that defaults to the indent string of the previous level or IndentedIO.default_string if at the first level. It is also possible to specify the indentation string using the symbolic argument :string. If level is negative, the text will be outdented instead:

$stdout.puts 'Not indented'
$stdout.indent(2, '> ') do |f|
  f.indent(string: '* ').puts 'Indented three levels'
  f.indent(-1).puts 'Indented one level'
end

# Not indented
# > > * Indented three levels
# > Indented one level

When text is outdented, the indentation string defaults to the previous level's indentation string - not the parent's

Kernel#indent {}

If given a block without an argument, Kernel#indent manipulates $stdout so that Kernel#print, Kernel#printf, Kernel#puts, and Kernel#p will output indented within that block:

puts 'Not indented'
indent do
  puts 'Indented one level'
  indent do
    puts 'Indented two levels'
  end
  puts 'Indented one level'
end
puts 'Not indented'

# Not indented
#   Indented one level
#     Indented two levels
#   Indented one level
# Not indented

Because this manipulates $stdout, the indentation carries through to methods that doesn't even know about IndentedIO:

def legacy(phrase)
  puts phrase
end

legacy('Not indented')
indent { legacy('Indented' }

# Not indented
#   Indented

This is probably the style that'll be used most of the time. It is of course still possible use Kernel#indent with a block argument if needed

bol - Beginning-Of-Line argument

#indent takes a symbolic :bol argument (true or false, default true) that specify if the output device is at the beginning of a line and that printing should start with an indentation string:

indent(1, bol: true).puts 'Indented'
indent(1, bol: false).puts 'Not indented\nIndented'

#   Indented
# Not indented
#   Indented

Constants

The default indentation string is defined in IndentedIO:

IndentedIO.default_indent = '>> '
indent.puts "Indented by #{IndentedIO.default_indent.inspect}"

# >> Indented by ">> "

The default at start-up is two spaces. It should normally be set only once at the start of the program. Use the indent string argument to #indent to get a different indentation for a part of the program

Exceptions

In case of errors an IndentedIO::Error exception is raised

Adding support for other classes

You can add support for your own IO objects by including IndentedIO::IndentedIOInterface in your class. All that is required is that the class define a #write method with the same semantics as IO#write (convert arguments to strings and then write them)

require 'indented_io'
class MyIO
  include IndentedIO::IndentedIOInterface
  def write(*args) ... end
end

my_io = MyIO.new
my_io.puts 'Not indented'
my_io.indent.puts 'It works!'

# Not indented
#   It works!

Implementation & performance

IndentedIO is intrusive because it extends the standard classes Kernel, IO, and StringIO with the #indent method. In addition, Kernel#indent with a block without parameters manipulates $stdout, replacing it with an IndentedIO object for the duration of the block

The implementation carries no overhead if it is not used but the core indentation mechanism processes characters one-by-one which is about 7-8 times slower than a handwritten implementation (scripts/perf.rb is a script to check performance). It would be much faster if the inner loop was implemented in C. However, we're talking micro-seconds here: Printing without using IndentedIO range from around 0.25us to 1us while using IndentedIO slows it down to between 2us and 8us, so IndentedIO won't cause a noticeable slow down of your application unless you do a lot of output

Installation

Add this line to your application's Gemfile:

gem 'indented_io'

And then execute:

$ bundle

Or install it yourself as:

$ gem install indented_io

Documentation

API documentation is on Rubydoc

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/clrgit/indented_io.

License

The gem is available as open source under the terms of the MIT License.