Templates and the Template Language T24

Name

t24 -- Templates and the Template Language T24

Overview

You may ask: Why a template language if we have PHP? Isn't it a template language itself? Good question! Of course, once PHP started as a template language and was designed just to be something like executable HTML. Years are gone and nowadays PHP is a powerful scripting language with a huge standard library and an even greater number of extensions. Today it is bad practice to mix PHP and (X)HTML since web development is all about separating code, design and content. In a project the designer and the developer may be separate persons and the designer may not know enough about PHP. In another project it may be a considderation of security not to let those who write a template also have complete access to the internals of the application or to the backend. PHP is still not supporting sandboxes. This is why we need a special template language.

T24 is simple and straightforward and is part of the Ister PHP Framework. It is the main goal to keep the language simple. T24 must not grow over the time to become a new scripting language implemented in a scripting language. This is why the current state of the language is frozen. New functions will only be added if it is possible to get them nearly for nothing (like :replace or :dump, added in 0.5.0).

Grammar

The parser is implemented as a recursive descent parser. The grammar is kept simple and in a way customizable.

Here we write the T24 grammar in EBNF notation. We use character classes to keep the notation small.

STREAM    = TEXT*, TAG*, TEXT*
TEXT      = ASCII*
TAG       = ( OPEN, CLOSE ) | ( OPEN, ( FUNC | VAR ), CLOSE )
OPEN      = NONSPACE+
CLOSE     = NONSPACE+
FUNC      = SPACE*, FUNCSEP, FUNCNAME, SPACE+, PARAMS*, SPACE*
FUNCSEP   = PUNCT
FUNCNAME  = FCHARS+
VAR       = VARNAME, SPACE+, PARAMS, SPACE*
VARNAME   = FCHARS, VCHARS*
PARAMS    = PARAM | ( PARAM, SPACE+, PARAMS )
PARAM     = STRING | NAME;
NAME      = NCHARS
STRING    = STRINGSEP, NCHARS*, STRINGSEP
STRINGSEP = "'" | '"'
NCHARS    = "[^'"]"
FCHARS    = "[A-Za-z]"
VCHARS    = "[_A-Za-z1-9]"
SPACE     = "[:space:]"
NONSPACE  = "[:^space:]"
PUNCT     = "[:punct:]"
ASCII     = "[:ascii:]"

The values for OPEN, CLOSE and FUNCSEP are not given by the grammar but may be customized with the setProperty() method of IsterTemplate. This is very useful if the parser is used to parse templates written in another format as XHTML or XML, such as plain text, TEX or uncompressed PDF. The default values are given below.

OPEN    = "<?t24"
CLOSE   = "?>"
FUNCSEP = ":"

The scope of variables is set to block level. This means that an enclosing block will disclose all of its assignments to the enclosed blocks. For example, variables outside of a repeat block will be known inside of this block, but the variables of the repeat block itself will not be known outside of this block.

Buildin functions

The T24 parser has a number of buildin functions.

:set

Assign a value to a variable from within the template.

<?t24 :set scalar 1 ?>
<?t24 :set array 1 2 3 4 ?>
<?t24 :set string1 '1 2 "3" 4' ?>
<?t24 :set string2 "1 2 '3' 4" ?>

:include

Include and parse another template from file or string.

<?t24 :include <path> [file] ?>
<?t24 :include <methodname> string [<arg1> [<arg2> [...]]] ?>

:replace

Include and do not parse another template.

<?t24 :replace <path> [file] ?>
<?t24 :replace <methodname> string [<arg1> [<arg2> [...]]] ?>
                       

:if

Evaluate true if condition is met.

<?t24 :if SAY_IT eq yes ?>Hello World!<?t24 :end ?>

:not

Evaluate true if condition is not met.

<?t24 :not SAY_IT eq no ?>Hello World!<?t24 :end ?>

Tests to use in :if and :not

There are different tests for strings and integers.

<?t24 :if <var>               ?> // var is true in respect to PHP rules
<?t24 :if <var> def           ?> // var is defined
<?t24 :if <var> z             ?> // var has strlen zero
<?t24 :if <var> nz            ?> // var has a non zero strlen
<?t24 :if <var> eq  <string>  ?> // var eaquels string
<?t24 :if <var> neq <string>  ?> // var not equals string
<?t24 :if <var> ==  <integer> ?> // var eaquels integer
<?t24 :if <var> !=  <integer> ?> // var not eaquels integer
<?t24 :if <var> >   <integer> ?> // var is greater than integer
<?t24 :if <var> <   <integer> ?> // var is less than integer
<?t24 :if <var> >=  <integer> ?> // var is greater or eaqual integer
<?t24 :if <var> <=  <integer> ?> // var is less or eaqual integer

:else

Define an alternative.

<?t24 :if THIS == 1 ?>
<?t24 DO_THIS ?>
<?t24 :else ?>
<?t24 DO_THAT ?>
<?t24 :end ?>

:repeat

Define a repeat block.

<?t24 :repeat repeater ?>
<?t24 COUNTER ?>
<?t24 :end ?>

In the example above, "repeater" must be a registered callback method returning an array of name value pairs. Each array then represents one row of a data set. The callback must return null if no more rows are left in the data set.

class Repeat extends IsterObject {

    var $counter = 10;
    
    function Repeat {
        parent::IsterObject();
    }

    function repeater {
        if(! $counter)
            return null;
        return array('COUNTER' => --$this->counter);
    }
}

:end

End the definition of a block like a conditio or a repeat.

<?t24 :if VAR == 1 ?>
<h1>Yippie Yeah!</h1>
<?t24 :end ?>

<table>
<?t24 :repeat list ?>
<tr><td><?t24 FOO ?></td><td><?t24 BAR ?></td></tr>
<?t24 :end ?>
</table>

:invoke

Invoke a registered method.

<?t24 :invoke example arg1 arg2 arg3 ?>
<?t24 :invoke example "a string" ?>
<?t24 :invoke example arg1=value "arg2=a string" ?>

The method must be defined and registered as a method of an extension of IsterObject. The parameters will be passed to this method in a single array. You may realize assignments by yourself as shown in the third example but note the use of quotes to make assignment of strings possible.

class Example extends IsterObject {
    
    function Example {
        parent::IsterObject();
    }

    function example($params) {
        foreach( $params as $param ) {
            if( preg_match('^(.+)=(.+)$', $param, $matches) )
                list($name, $value) = $matches;
            else
                print $param;
        }
    }
}

:dump

The :dump function prints a representation of all variables currently defined in the template object. This representation is formatted as an associative PHP array. The function may be useful for debugging or during development when the template author has not yet finished work and the PHP developer needs to test a form or something.

<?t24 :dump ?>

Note: currently this only works outside of repeat blocks.

Using the template classes

A simple example

As a very simple example we use the following html template and save it as simple.tmpl.

<html>
<head>
<title><?t24 TITLE ?></title>
</head>
<body>
<h1><?t24 TITLE ?></h1>
</body>
</html>

To parse this template and send the resulting document to the client all you need are the following lines.

<?php
require_once('IsterTemplate.php');
require_once('IsterTemplateFactory.php');
$t = new IsterTemplate( new IsterTemplateFactory );
$t->setLanguage('t24');
$t->setProperty('/template', array('TITLE' => 'Test Template');
$t->parse('simple.tmpl');
print $t->toString();
?>

And this is what the client sees.

<html>
<head>
<title>Test Template</title>
</head>
<body>
<h1>Test Template</h1>
</body>
</html>

Not only files but also strings may be used as templates. That way it is possible to store templates in databases. To tell the template object that the string is not the name of a file, the constant ISTER_TEMPLATE_STRING must be added to the method call.

$t->setProperty('/template', array('WHO' => 'World'));
$t->parse('Hello <?t24 WHO ?>!', ISTER_TEMPLATE_STRING);
print $t->toString();

Assigning values to variables

To assign one or more values to a template variable the setProperty method of IsterTemplate is used. The path must be written as "/template" and an additional array with name value pairs must be passed. It is very easy to populate a whole IsterAttributeSet or an IsterSqlDataObject (which extends IsterAttributeSet) in one line.

<?php
$data = new IsterSqlDataObject;
// do some more setup for this object here
$data->select();

$t    = new IsterTemplate( new IsterTemplateFactory );
$t->setLanguage('t24');
$t->setProperty('/template', $data->getAttributesArray());
$t->parse('form.tmpl');
print $t->toString();
?>

This is an example of a callback method and will simply print out "Hello World" twice.

<?php
require_once('IsterTemplate.php');
require_once('IsterTemplateFactory.php');

class Example extends IsterObject {
    
    function Example {
        parent::IsterObject();
    }

    function example($params) {
        print $params[0];
    }
}

$e = new Example;
$t = new IsterTemplate( new IsterTemplateFactory );
$t->setLanguage('t24');
$t->setProperty('/register/method', array('example' => &$e);
$t->setProperty('/register/alias',  array('example' => 'print');
$t->parse('<?t24 :invoke example "Hello World"?>
<?t24 :invoke print "Hello World"?>', 
ISTER_TEMPLATE_STRING);
print $t->toString();

?>

Including Other Templates

There are two different buildin functions to include other templates: :include and :replace. This may be done up to any nesting level. The only difference between these two functions is that an included template is parsed by the template parser and variables are resolved while a replaced template is not parsed and variables are not resolved. This may be used to tune performance.

==== t1.tmpl
<?t24 HELLO ?>
====

==== t2.tmpl
World
====

==== t3.tmpl
<?t24 :include t1.tmpl ?> <?t24 :replace t2.tmpl ?>
====

To :include or :replace string templates a callback method must be used, followd by the keyword "string" and any number of arguments.

==== t3.tmpl
<?t24 :include getString string hello ?> 
<?t24 :replace getString string who ?>
====

A Complex Template

Now we can write a more complex template like this.

<html>
  <head>
  <?t24 :include head.tmpl ?>
  </head>
  <body>
  <?t24 :replace javascript.tmpl ?>
  <h1><?t24 title ?></h1>
  <?t24 :if datacount > 0 ?>
  <table>
    <tr>
      <th>name</th>
      <th>city</th>
      <th>birthday</th>
    </tr>
    <?t24 :repeat datarows ?>
    <tr>
      <td><?t24 name ?></td>
      <td><?t24 city ?></td>
      <td><?t24 birthday ?></td>
    </tr>
    <?t24 :end ?>
  </table>
  <?t24 :else ?>
  <p>No data found.</p>
  <?t24 :end ?>
  </body>
</html>

Customizing the template engine

The following example shows the default setup of the parser.

<?php
require_once('IsterTemplate.php');
require_once('IsterTemplateFactory.php');
$t = new IsterTemplate( new IsterTemplateFactory );
$t->setLanguage('t24');
$t->setProperty('/parser', 
                array(// include path of templates
                      'include_path'        => '.',
                      // function marker sign
                      'function_separator'  => ':',
                      // open a template tag here
                      'tag_open'            => '<?t24',
                      // close a template tag here
                      'tag_close'           => '?>',
                      // start comment in target format
                      'comment_open'        => '<!--',
                      // end comment in target format
                      'comment_close'       => '-->',
                      // what to do with unregistered variables
                      'policy_unregistered' => t24SKIP  
                     )
              );
?>

The include_path may be used to setup an environment even with different locations of template files (string templates are not searched in a path). The path may be written as usual for PHP include paths as a colon separated list on Unix/Linux and as a semicolon separated list on Windows.

If you are about to write XHTML templates and you are using a WYSIWYG editor you may decide to change the values for tag_open and tag_close.

<?php
$t->SetProperty('/parser', 
                array('tag_open'   => '{',
                      'tag_close'  => '}'
                     )
              );
?>

Now the template tags will show up even in WYSIWYG mode.

<h1>{ HELLO_WORLD }</h1>

But, of course, this is not that useful for TEX/LATEX - and Javascript.

Unregistered variables are variables occuring in a template but were not been registered previously to the template parser. With the policy_unregistered property you can decide what to do with such variables. The default is set to t24SKIP which will silently ignore such variables. Other possible values are t24WARN (trigger a warning for unregistered variables), t24KEEP (keep the variable name unchanged in the output string) and t24COMMENT (comment the variable according to the target format). For the latter you can adjust the comments with the properties comment_open and comment_close

Using other template languages

The main template class IsterTemplate of the Ister PHP Framework is not restricted to use T24 as the template language. It may use any other language as long as this language is accessible through a class implementing the interface iIsterParser. The parserSetProperty() method of the parser should understand the paths as described above. In PHP4 the new parser must additionally extend IsterParser. The interface is declared in the following way.

interface iIsterParser {
     function getIncluded();
     function getName();
     function getParseResult();
     function parseFile($path);
     function parseString($string, $name);
     function parserSetProperty($path, $array);
     function setParseResult($result);
     function toString();
}

Links

Other Documents

Table of contents