tmpl_parser

Description

The engine works by converting a template (which is a string, or a file) into a Tcl script, and then running it. Each line of text encountered will be returned as is. Exception is text between <% ... %> which is treated as Tcl code (the eval happens in a regular interp).

Why not expand?

expand is a simple and snugly for Tcl language, be mounted in tcllib. You can use it as quick as flash.

But, that library has a problem, when to really use. We cannot write easily following signs

 [ ] { }

DDG No! We can easily write

 [ ] { }

with expand if we use alternative brackets for expansion:

 (Tclkit) 24 % package require textutil::expander
 1.3.1
 (Tclkit) 25 % textutil::expander exp
 ::exp
 (Tclkit) 26 % set string {[] {} <% set x %> }
 [] {} <% set x %> 
 (Tclkit) 27 % set x 65 
 65
 (Tclkit) 28 % exp expand $string {<% %>}
 [] {} 65 

kanryu Oh, I didn't know to be able to change brackets with textutil::expander. Then, there are restricted cases to use this library, maybe.

Why not TemplaTcl?

TemplaTcl is a powerful and cool template engine.

But now, we cannot use it on Tcl8.4 :(

tmpl_parser is

a simple and fast template engine. That still not have function to read a file, create a parser object.

But we can use it instant, only write a command require or source.

Simple variable substitution is done with "<%= $variable %>" (e.g., Thanks <%=$count%> accesses!).

Simple expression substitution is done with "<%:Tcl expression%>" (e.g., <%:$i+2000%>).

Usage

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

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

 <ul>
   <% foreach item $cityList {
     %><li><%= $item %>
   <% } %>
 </ul>

The above template (in HTML language) generates a 4-row table and an unordered list, taking values from a $cityList variable that we're going to define. First let's load the code:

 source tmpl_parser.tcl

reading template file.

 set fd [open templtest.tmpl r]
 set tmpl [read $fd]
 close $fd

now create a parser instance:

 set parser [::tmpl_parser::tmpl_parser $tmpl]

set the variables used in our template:

 set cityList {Ragusa Ravenna Rieti Rimini Rome Rovigo}

and render the template:

 puts [eval $parser]

Here's the output that gets produced:

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

 <ul>
   <li>Ragusa
   <li>Ravenna
   <li>Rieti
   <li>Rimini
   <li>Rome
   <li>Rovigo
 </ul>

Code

 # tmpl_parser.tcl
 #
 #  Tcl embeddedd script parser(a template engine)
 #
 #  This module comverts Tcl embedded scripts into a Tcl normal script(parser),
 #  after you just have to do eval command for the generated parser.
 #
 # Copyright (c) 2007 by Kanryu KATO<[email protected]>
 # licensed on Tcl License.
 
 package require Tcl 8.3
 package provide tmpl_parser 0.1
 
 namespace eval ::tmpl_parser {
     namespace export tmpl_parser
 }
 
 proc ::tmpl_parser::tmpl_parser {tmpl} {
     # Tcl enbedded tags
     # [[outer <%...inner...%> outer]] <-$tmpl
     #  [        =         ]           <-$token
     #        cd ef        hi j        <-indexes
     set parser { {set _o {}} }
     while {[set i [string first %> $tmpl]] != -1} {
         set h [expr {$i-1}]
         set j [expr {$i+2}]
         set token [string range $tmpl 0 $h]
         set d [string first <% $token]
         set c [expr {$d-1}]
         set e [expr {$d+2}]
         set f [expr {$d+3}]
         
         # outer
         lappend parser [escaped_parse [string range $token 0 $c]]
         switch [string index $token $e] {
             "=" {
                 # normal expression (e.g., Thanks <%=$count%> accesses!)
                 lappend parser [normal_parse [string range $token $f end]]
             }
             ":" {
                 # numeric expression (e.g., <%:$i+2000%>)
                 lappend parser [numeric_parse [string range $token $f end]]
             }
             default {
                 # embedded Tcl command is passed through listing
                 lappend parser [string range $token $e end]
             }
         }
         # after "%>"
         set tmpl [string range $tmpl $j end]
     }
     #last outer
     lappend parser [escaped_parse $tmpl]
     lappend parser {join $_o ""}
     
     return [join $parser "\n"]
 }
 
 proc ::tmpl_parser::escaped_parse {str} {
     set str [string map {\" \\\" \{ \\\{ \} \\\} \\ \\\\} $str]
     return "lappend _o \"$str\""
 }
 
 proc ::tmpl_parser::normal_parse {str} {
     return "lappend _o $str"
 }
 
 proc ::tmpl_parser::numeric_parse {str} {
     return "lappend _o [expr $str]"
 }

milarepa - 2014-07-20 17:26:11

I been using tmpl_parser to parse a configuration file. So far it works fine. My problem now is that when I insert a variable like this:

<%= $variable_name %>

Inside that $variable_name there is a plain text that containes another tag to process another variable:

<%= $variable_inside_a_variable %>

I understand when the parser processes $variable_name will print $variable_inside_a_variable literally without doing the variable substitution.

Is there a way to do exactly that?


milarepa - 2014-07-20 17:35:05

I just go it !!!

Put the following lines to parse the variable once more:

set variable_inside_a_variable [eval [::tmpl_parser::tmpl_parser $variable_inside_a_variable]]

Another implementation

dbohdan 2014-08-04: I've reimplemented tmpl_parser with regexp and Tcl's built-in quoting used for code generation (I found the original's approach to quoting a bit of a problem when trying to extend it). In this version <%= ... %> can be used for both variable substitution and expressions, which are evaluated a run time; parse-time expression evaluation that the original used <%: ... %> for is not present. A new tag pair <%! ... %> is introduced: <%! command %> translates to code to insert the return value of command into the output. The motivation is that it is shorter than saying <%= command %> and eliminates unnecessary expression evaluation.

Caveat: I am not sure what is the oldest Tcl version that will run this implementation; it runs in 8.5.

Code

# Convert a template into Tcl code.
proc parse {template} {
    set result {}
    set regExpr {^(.*?)<%(.*?)%>(.*)$}
    set listing "set _output {}\n"
    while {[regexp $regExpr $template \
            match preceding token template]} {
        append listing [list append _output $preceding]\n
        switch -exact -- [string index $token 0] {
            = {
                append listing \
                        [format {append _output [expr %s]} \
                                [list [string range $token 1 end]]]
            }
            ! {
                append listing \
                        [format {append _output [%s]} \
                                [string range $token 1 end]]
            }
            default {
                append listing $token
            }
        }
        append listing \n
    }
    append listing [list append _output $template]\n
    return $listing
}

Example

The <table> and <ul> examples from "Usage" work as is. What follows is a more in-depth example that comes from Tclssg. It employs the new <%! ... %> feature and generates HTML for use with Bootstrap.

<%

interp-source default-procs.tcl

%><!DOCTYPE html>
<html>
  <head>
    <meta charset="<%! website-var-get-default charset UTF-8 %>">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">
    <!-- <link rel="icon" href="<%= $rootDirPath %>/favicon.ico"> -->

    <title><%! format-html-title %></title>

    <!-- Bootstrap core CSS -->
    <link href="<%= $rootDirPath %>/external/bootstrap-3.2.0-dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- Bootstrap theme -->
    <link href="<%= $rootDirPath %>/external/bootstrap-3.2.0-dist/css/bootstrap-theme.min.css" rel="stylesheet">
    <!-- Custom styles for this template -->
    <link href="<%= $rootDirPath %>/tclssg.css" rel="stylesheet">
    <%! page-var-get-default headExtra "" %>
  </head>

  <body>
    <div class="navbar navbar-default">
      <div class="container">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" href="#"><%= $websiteTitle %></a>
        </div>
        <div class="navbar-collapse collapse">
          <ul class="nav navbar-nav">
          <li><!-- class="active" --><a href="<%= $indexLink %>">Home</a></li>
          <li><a href="<%= $blogIndexLink %>">Blog</a></li>
          <li><a href="<%= $rootDirPath %>/contact.html">Contact</a></li>
          </ul>
        </div><!--/.nav-collapse -->
      </div>
    </div>


    <div class="container">
      <div class="row">
        <% if {[page-var-get-default blogPost 0] && \
              (![page-var-get-default hideSidebar 0] || \
                  [page-var-get-default showTagCloud 0])} { %>
          <div class="col-md-8">
            <%= $content %>
            <%! format-prev-next-links {Previous page} {Next page} %>
          </div>
          <div class="col-md-4 well">
            <%! format-sidebar %>
            <%! format-tag-cloud %>
          </div>
         <% } else { %>
          <div class="col-md-12">
            <%= $content %>
            <%! format-prev-next-links {Previous page} {Next page} %>
          </div>
        <% }
        %>
        <div>

        </div>
      </div>


      <%! format-comments %>


      <div class="footer">
        <%! format-footer %>
      </div>

    </div> <!-- /container -->



    <!-- Bootstrap core JavaScript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <script src="<%= $rootDirPath %>/external/bootstrap-3.2.0-dist/js/bootstrap.min.js"></script>
</html>