GenTemplate

Koen Van Damme -- When generating code or other structured output, it can be useful to store the boilerplate in a template, and then configure it with specific data before producing the output. I made a small class for such templates. You give a template object a 'text' attribute containing the template text. The template can contain references to other attributes between at-signs, like this: 'My name is @[email protected]'. Then you just give values to those attributes (such as 'name') and then invoke the 'gen' method to produce the output. Very simple.

Here is an example. Note that we use a vertical bar '|' as a special mark to take care of indentation:

 GenTemplate g1 -prepend_indent ">> " -mark "|" -naam "Koen" -plek "world" -text {
    |Hello @[email protected],
    |   My name is @[email protected]
    |   I live here:
    |   @[email protected]

    |   If you did not hear that:  I LIVE HERE:
    |   !! @[email protected] !!
    |
    |That's all for today.
 }

 puts [g1 gen]

This produces the following output, as expected:

 >> Hello world,
 >>    My name is Koen.
 >>    I live here:
 >>    world
 >>
 >>    If you did not hear that:  I LIVE HERE:
 >>    !! world !!
 >>
 >> That's all for today.

Note that the indentation starts only after the vertical bars in the original template. Also note that we asked '>> ' to be printed before each line (the 'prepend_indent' attribute).

Now just change the values of 'naam' and 'plek' to produce different outputs. In case you wonder: those are the Dutch words for 'name' and 'place' ;-)

Here is an example of how it handles newlines inside attribute values:

 g1 set -plek "some planet\nfar, far,\nfar away"
 puts [g1 gen]

produces this:

 >> Hello some planet
 >> Hello far, far,
 >> Hello far away,
 >>    My name is Koen.
 >>    I live here:
 >>    some planet
 >>    far, far,
 >>    far away
 >>
 >>    If you did not hear that:  I LIVE HERE:
 >>    !! some planet
 >>    !! far, far,
 >>    !! far away !!
 >>
 >> That's all for today.

You can see that it repeats all characters before the injected content on every line. This is sometimes what you want (e.g. you want a '*' in front of every line of generated documentation in C), sometimes not (as with the repeated 'Hello' above).

If the replaced contents in turn contain at-signs, you can configure the template object to replace those recursively:

 # First time: do not recurse.
 g1 set -plek "castle of @[email protected]"
 puts [g1 gen]

 # Now: ask it to recurse, replacing '@[email protected]' in a second pass.
 g1 set -recurse true
 puts [g1 gen]

produces this:

 >> Hello castle of @[email protected],
 >>    My name is Koen.
 >>    I live here:
 >>    castle of @[email protected]
 >>
 >>    If you did not hear that:  I LIVE HERE:
 >>    !! castle of @[email protected] !!
 >>
 >> That's all for today.

 >> Hello castle of Koen,
 >>    My name is Koen.
 >>    I live here:
 >>    castle of Koen
 >>
 >>    If you did not hear that:  I LIVE HERE:
 >>    !! castle of Koen !!
 >>
 >> That's all for today.

In addition to pasting attribute values, you can also paste the entire contents of another template, by referencing it with '@> [email protected]' syntax. Here is a second template object that refers to the first one, showing off the correct handling of indentation and prefixes:

 GenTemplate g2 -prepend_indent "++ " -mark "+" -sender "Some weirdo" -text {
    + I got the following message from @[email protected] (indented for readability):

    +    @>[email protected]

    + Do you want to reply to @[email protected] or not?  If so, here is the message
    + again, prefixed with a reply mark:

    +    >@>[email protected]

    + Don't mention it.
 }

 g1 set -plek "the Moon" -prepend_indent " * "
 puts [g2 gen]

This is the output:

 ++  I got the following message from Some weirdo (indented for readability):
 ++
 ++      * Hello the Moon,
 ++      *    My name is Koen.
 ++      *    I live here:
 ++      *    the Moon
 ++      *
 ++      *    If you did not hear that:  I LIVE HERE:
 ++      *    !! the Moon !!
 ++      *
 ++      * That's all for today.
 ++
 ++
 ++  Do you want to reply to Some weirdo or not?  If so, here is the message
 ++  again, prefixed with a reply mark:
 ++
 ++     > * Hello the Moon,
 ++     > *    My name is Koen.
 ++     > *    I live here:
 ++     > *    the Moon
 ++     > *
 ++     > *    If you did not hear that:  I LIVE HERE:
 ++     > *    !! the Moon !!
 ++     > *
 ++     > * That's all for today.
 ++     >
 ++
 ++  Don't mention it.

All that remains now is the implementation of the template class. The class is based on my Oblets mechanism, but you can easily port it to any other Tcl object system.

 oblet_class GenTemplate {
    set text ""
       # The text of the template.  In this text:
       # - Leading indentation up to and including 'mark' is removed
       #   from every line.
       # - 'prepend_indent' is prepended to every line.
       # - Every @[email protected] is replaced by the value of data member 'xxx'
       #   of this GenTemplate object.
       # - Every @>[email protected] is replaced by the results of calling 'gen'
       #   on another object globally named 'xxx'.
       # - When the replacing value is multi-line, it gets the
       #   beginning of the original line prepended to every subline.
       #   Just try it, you'll see.

    set prepend_indent ""
       # The indentation string prepended to every line of the output.
    set mark ""
       # The mark which indicates the start of a line in 'text'.
       # Every non-empty line MUST start with this mark, if any.
       # The mark may contain more than 1 character.
    set recurse "false"
       # When 'true', the @-signs in the replaced stuff are
       # replaced in turn, recursively.

    proc construct_text {var lead} {
       if { [regexp {^>(.*)$} $var -> obj_name] } {
          set result [$obj_name gen]
             # This can be another GenTemplate object, or any object
             # that happens to implement a 'gen' method!
       } else {
          $this members $var
          set result [set $var]
       }

       set new_result ""
       set is_first true
       foreach line [split $result "\n"] {
          if { $is_first == "false" } {
             append new_result "\n"
          }
          append new_result "${lead}${line}"
          set is_first false
       }
       return $new_result
    }

    proc gen {} {
       $this members text prepend_indent mark recurse

       regsub -all { *$} $prepend_indent "" blank_indent
       if { $mark != "" } {
          set mark_exp "^ *[escape_funny $mark regexp](.*)\$"
       }

       set result ""
       set is_first true
       set blank_lines ""
       foreach line [split $text "\n"] {
          if { [regexp {^ *$} $line] } {
             append blank_lines "${blank_indent}\n"
             continue
          }

          if { $mark != "" } {
             # Every non-empty line MUST start with the mark.
             # Spaces before the mark and the mark itself are thrown away.
             if { ![regexp $mark_exp $line -> rest] } {
                panic "Template lines must begin with mark '$mark'."
             }
             set line $rest
          }

          if { [regexp {^ *$} $line] } {
             append blank_lines "${blank_indent}\n"
             continue
          }

          if { $is_first == "false" } {
             append result $blank_lines
          }
          set blank_lines ""
          set is_first false

          if { $recurse == "true" } {
             while { [regexp {^([^@]*)@([^@]+)@(.*)$} $line -> first var next] } {
                set line "[$this construct_text $var $first]${next}"
             }

          } else {
             set next $line
             set line ""
             while { [regexp {^([^@]*)@([^@]+)@(.*)$} $next -> first var new_next] } {
                append line "[$this construct_text $var $first]"
                set next $new_next
             }
             append line $next
          }

          # If the pasted stuff contains multiple lines, we need to
          # prepend them correctly!  Thankfully, 'regexp' crosses line boundaries
          # (unless the -line option is given), so the code above does what it needs to.
          foreach subline [split $line "\n"] {
             append result "${prepend_indent}${subline}\n"
          }
       }

       return $result
    }
 }