Version 99 of TemplaTcl: a Tcl template engine

Updated 2007-05-30 18:10:33 by escargo

FF 2007-30-05 - The discussion evolved in ToW Tcl on WebFreeWay has generate some interest, and here is an effort for making a minimal template engine. I would like to use this page for collecting ideas from people. Fell free to change everything, fix bugs and revolutionize the concept - it is the purpose of the page.

Here's how it is (initially) supposed to work:

first, let's create a template file (templtest.tmpl):

 <table><%
   for {set i 0} {$i < 4} {incr i} { %>
   <tr>
     <td><%= $i %></td>
   </tr><% 
   } %>
 </table>

now run it:

 source TemplaTcl
 TemplaTcl::parseFile templtest.tmpl
 TemplaTcl::dump

and here's the output that gets produced:

 <table>
   <tr>
     <td>0</td>
   </tr>
   <tr>
     <td>1</td>
   </tr>
   <tr>
     <td>2</td>
   </tr>
   <tr>
     <td>3</td>
   </tr>
 </table>

Description: really, this engine it works by converting the whole template into a Tcl script, and then running it. This approach maybe it has pro and cons. I see the only pro that is very flexible and it leaves you maximum freedom of doing what you want, dealing with any data and putting how much code you want in the template (although the template should possibly hold minimum code - according to MVC, just the presentation logic, no code related to controller logic, state persistance, or data-retrieval logic).

The above example generates this "middle" code:

 puts -nonewline <table>

   for {set i 0} {$i < 4} {incr i} {
 puts -nonewline {
   <tr>
     <td>}
 puts -nonewline  $i
 puts -nonewline {</td>
   </tr>}

   }
 puts -nonewline {
 </table>
 }

(you can see that by replacing eval with puts)


 #!/usr/bin/env tclsh

 # tcllib required
 package require Tcl 8.2
 package require struct

 namespace eval TemplaTcl {
        variable data
        variable mode
        variable modeprev

        proc parseFile {file} {
                set fh [open $file r]
                set raw [read $fh]
                close $fh
                return [parse $raw]
        }

        proc parse {template} {
                variable data
                variable mode
                variable modeprev
                set mode raw
                mode raw
                ::struct::queue cc

                set rawl [split $template {}]
                foreach ch $rawl {
                        # we work char-by-char :|
                        cc put $ch

                        # max block to compare (<%=) is 3 chars long:
                        if {[cc size] >= 3} {
                                set s3 [join [cc peek 3] {}]
                                set s2 [join [cc peek 2] {}]
                                if {$mode == "raw"} {
                                        if {$s3 == "<%="} {
                                                # <%= is a shorthand for puts ...
                                                cc get 3
                                                mode code
                                                buf "puts -nonewline "
                                                continue
                                        } elseif {$s2 == "<%"} {
                                                # <% indicates begin of a code block
                                                cc get 2
                                                mode code
                                                continue
                                        }
                                } elseif {$mode == "code"} {
                                        if {$s2 == "%>"} {
                                                # and %> is the end of code block
                                                cc get 2
                                                mode raw
                                                continue
                                        }
                                }
                                buf [cc get]
                        }
                }
                # finish working on the queue:
                while {[cc size] > 0} {
                        buf [cc get]
                }
                mode flush
        }

        proc mode {m} {
                # used internally by parse - switches mode and stuff...
                variable data
                variable mode
                variable modeprev

                set newm {}
                switch $m {code - raw {set newm $m}}
                if {$newm != {}} {
                        set modeprev $mode
                        set mode $newm
                        set data(buf:$mode) {}
                }
                if {$m == "flush"} {set modeprev $mode ; set mode _}
                if {$mode != $modeprev} {
                        lappend data(out) [list $modeprev $data(buf:$modeprev)]
                        set data(buf:$modeprev) {}
                }
        }

        proc buf {ch} {
                # used internally by parse - put $ch in the right buffer
                variable data
                variable mode
                append data(buf:$mode) $ch
        }

        proc dump {} {
                # run the template script
                variable data
                set tclBuf ""
                foreach l $data(out) {
                        set t [lindex $l 0]
                        set d [lindex $l 1]
                        switch $t {
                                raw {append tclBuf [list puts -nonewline $d]\n}
                                code {append tclBuf $d\n}
                        }
                }
                eval $tclBuf
        }
 }

escargo - I think it's a mistake to have parse read a file; it would be potentially more flexible to use if it just took a string to process as input, and let the caller determine where the string comes from. FF - good point! (changed)

APW Just found textutil::expander[L1 ] package in tcllib, which is included in ActiveTcl 8.4.14.0. Have the feeling it does something similar, maybe it's worth looking at it.


[ Category Application ]