Code Indentation

Logical and consistent indentation is a major factor in code clarity. Indentation can emphasize code's structure, making it easier to understand. This pays off handsomely in development activities such as debugging, enhancement, and refactoring.

Readers of both manually- and mechanically-generated code can benefit from effective use of indentation. Finally, in some languages (eg, CoffeeScript, HAML, SASS, YAML) indentation changes can change the meaning of the code.

SketchUp plugins that use WebDialogs may generate substantial amounts of code (eg, CSS, HTML, JavaScript). The tools and techniques described below can help programmers to keep this code indented in a regular and meaningful manner.

Problem Statement

Ruby methods and "here documents" appear to provide an easy way to generate code. For example, this technique can generate both simple and nested HTML:

def foo                  def bar
  <<-EOT                   <<-EOT
<tr>                     <table>
  <td>Hello,</td>          #{ foo }
  <td>World.</td>        </table>
</tr>                      EOT
  EOT                    end
end

Unfortunately, the output from this naive approach mangles our carefully edited indentation:

<table>
  <tr>
  <td>Hello,</td>
  <td>World.</td>
</tr>

</table>

Specifically, only the first line from foo is indented to the initial level. The following lines lose this indentation, retaining only the indentation defined in foo. An extra blank line also appears before the closing table tag.

Workaround

We can work around this problem by using a combination of tricks:

  • start with a single-quoted here document:   <<-'EOT'

  • modify the captured string:   @pf.indent(<<-'EOT')

  • evaluate the modified string:   eval @pf.indent(<<-'EOT')

Using this approach, the following code generates a properly indented string:

def bar
  tmp_foo = foo

  out = eval @pf.indent(<<-'EOT')
<table>
  #{ tmp_foo }
</table>
  EOT
end

Explanation

@pf.indent adds some code (eg, @pf.indent_all calls) to the captured string. When run by eval, this code adds "appropriate" indentation, performs to_s coercion, etc.

Because eval runs in the context of the enclosing code, it can expand "#{ tmp_foo }" with the local value of "tmp_foo". Of course, the usual caveats apply to handing unfiltered code to eval!

Thanks to Caleb Clausen for the cool and concise (if weird :-) "@pf.indent(<<-'EOT')" syntax.

Usage Notes

Using these methods properly is critical to achieving good results. Here are some caveats and hints. Please let me know if you have any additions or questions.

Minimize evaled Code

The use of temporary variables (eg, tmp_foo) is not accidental. The handling of traceback information from exceptions is a bit brittle, at least in Ruby 1.8.6 (as used by SketchUp 8.*). More generally, unnecessary code evaluation wastes processing time and makes debugging more complicated.

Let's say that you call method foo inside the eval. If foo (or something it calls) gets an exception, all Ruby will tell you is that the eval crashed. By calling all methods outside of the eval, we ensure that Ruby will provide normal traceback information.

Method calls aside, minimize the amount of Ruby code in the here document. This code must be parsed (etc) each time the eval runs, wasting processing time for no reason. Also, any tracebacks will be substantially more complex (and consequently, harder to understand).

Use with ERuby

The workaround can also be used in Embedded Ruby, as:

<%=
  tmp_foo = foo

  out = eval @pf.indent(<<-'EOT')
<table>
  #{ foo }
</table>
  EOT
%>

Implementation

  def indent(input)
  #
  # Pre-process lines from a quoted string (eg, here document),
  # adding code to indent the resulting code.
  #
  # input   - input text (eg, HTML or JavaScript code) {String}
  #
  # Returns a preprocessed String, for use by eval.

    # Split input into lines, then process (as needed)
    # to wrap included code in indent_all calls, etc.

    lines = []
    input.split("\n").each do |line|
      if line =~ /^( *)#\{\s*([^}]+?)\s*\}\s*$/
        level   = $1.size
        body    = "@pf.indent_all( #{ $2 }.to_s, #{ level } )"
        lines  << "\#{ #{ body } }"
      else
        lines  << line
      end
    end

    # Join processed lines, then add quoting syntax.
    # \006 is unlikely to conflict with any content.

    out = "%Q\006" + lines.join("\n") + "\006"
  end

  def indent_all(input, level, trim=nil)
  #
  # Helper method for indent.
  # Indents all lines by a specified amount.
  # Removes any trailing newline.
  #
  # input  - input text {String}
  # level  - desired indentation level {Fixnum}
  # trim   - trim indentation on first line {Boolean}
  #
  # Returns an indented (etc) version of self.

    spaces  = ' ' *  level
    out     = input.gsub(/^/, spaces).chomp
    trim ? out.sub(/^#{ spaces }/, '') : out
  end


This wiki page is maintained by Rich Morin, an independent consultant specializing in software design, development, and documentation. Please feel free to email comments, inquiries, suggestions, etc!

Topic revision: r5 - 02 Jan 2012, RichMorin
This site is powered by Foswiki Copyright © by the contributing authors. All material on this wiki is the property of the contributing authors.
Foswiki version v2.1.6, Release Foswiki-2.1.6, Plugin API version 2.4
Ideas, requests, problems regarding CFCL Wiki? Send us email