Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Templatest

Push-style (with pull methods for e.g. csrf & i18n) lazy topo-ordered incremental building, liberated from von-neumann-style applicative transition template engine for WackoWiki.

Templatest is a lightweight, compiled template engine that separates presentation from logic. Templates are compiled once into an internal AST of patterns, variables, subpatterns and filters, then rendered by walking the AST and applying transformations on demand.

Features

  • Compiled templates — parsed once, cached to disk, reused across requests
  • Pattern blocks — define and recall named sections ([= name =] ... [=])
  • One-off pattern definitions[= abc def = ... =] for inline sub-templates
  • Variables[' var '] with optional filter pipes (| upper | default "anon")
  • Subpatterns[ ' name ' ] to embed pattern instances
  • Pull actions[ '' name: arg1 '' ] to call user-supplied functions at render time
  • Filter pipeline — built-in filters for HTML/JS/CSS/URL escaping, string manipulation, dates, numbers, JSON encoding, and more
  • Auto-indent — block tags automatically align with their surrounding context
  • Pre-block support — special handling for <pre> and <textarea> content
  • Escaper — context-aware escaping (HTML, HTML attribute, JS, CSS, URL) based on Zend Framework
  • Pull-style extensibility — register runtime callbacks (CSRF tokens, i18n, etc.) that the template can invoke
  • PHP 8.1+ strict typing — fully typed, namespaced, framework-independent
  • No dependencies — zero Composer runtime dependencies; PHPUnit only for dev

Requirements

  • PHP 8.1 or later
  • iconv or mbstring extension (for non-UTF-8 encoding support in TemplatestEscaper)

Installation

composer require wackowiki/templatest

Or, if vendoring manually:

require __DIR__ . '/vendor/autoload.php';

Quick start

Template file hello.tpl

[= hello =]
<!DOCTYPE html>
<html>
<head><title>[ ' title ' ]</title></head>
<body>
<h1>Hello, [ ' name ' ]!</h1>
[ ' items ' ]
</body>
</html>

[= items =]
<ul>
[ ' item ' ]
</ul>
[=]

[= item =]
<li>[ ' text ' ]</li>
[=]

(Use [==== name ====] for visual emphasis — Templatest accepts any number of = signs.)

Rendering

use Templatest\Templatest;

$tpl = Templatest::read('hello.tpl', '/path/to/cache');

$tpl->title         = 'My Page';
$tpl->name          = 'World';
$tpl->items_item_text = 'first';   // underscore-split path: items → item → text
$tpl->items_item_text = 'second';  // second iteration of same pattern

echo $tpl;

Output

<!DOCTYPE html>
<html>
<head><title>My Page</title></head>
<body>
<h1>Hello, World!</h1>
<ul>
<li>first</li>
<li>second</li>
</ul>
</body>
</html>

Template syntax

Quote convention

Templatest tags are wrapped in ' (apostrophe) characters. You can use one or more quotes on each side — the parser treats them as visual delimiters only. The minimum is one:

[' var ']              ← one quote (recommended)
['' var '']            ← two quotes (works, no functional difference)
[''' var ''']          ← three quotes (works, but visually noisy)

For nesting clarity, it's common to use more quotes for outer context and fewer for inner context, though this is purely cosmetic:

[ '' pull_function: [' inner_var '] '' ]   ← pull with inner variable

Equal signs in pattern markers

Pattern blocks use [= name =] or [==== name ====] — any number of = signs works:

[= main =]
[==== main ====]
[================ main =================]

All three are equivalent. Use whichever reads best.

Pattern definition

[= pattern_name =]
... template body ...
[=]

The first pattern defined in a file is the main pattern and is the one rendered by default.

Inline (one-off) pattern

[= anon = ...inline body... =]

Useful for small reusable chunks. The name may be a single punctuation character (anonymous) or an identifier.

Variables

[' var_name ']
[' var_name | filter1 | filter2 ']

Variables are assigned from PHP via $tpl->var_name = 'value' or via underscore-split paths.

Subpatterns

[ ' sub_pattern ' ]
[ ' my_name sub_pattern ' ]

Embeds a pattern instance. Each invocation instantiates a fresh copy unless static inlining applies.

Pull actions

[ '' function_name '' ]
[ '' function_name: arg1 arg2 '' ]

Invokes a user-registered PHP callback. The callback receives:

function my_pull(bool $is_block, string $loc, ...$args): string

Filter pipes

[' msg | upper | trim ']
[' msg | default "anonymous" | escape ']

Filters are evaluated left-to-right. The result of each filter is the input to the next.

Setup directives

.escape html                    # default escape mode for this file
.escape html pattern_name       # escape mode for a specific pattern
.patch patname varname value    # pre-populate a variable in a pattern
.include other_template.tpl     # include another template file

Built-in filters

Filter Alias Description
escape e HTML-escape value (modes: html, js, css, url, html_attr, raw)
default Use fallback when value is null/false
format sprintf() the value
stringify Convert any value to a readable string
date Format a Unix timestamp
join Implode an array with a glue
lower mb_strtolower
upper mb_strtoupper
number number_format with custom decimal/thousands separators
void Drop the value (returns null)
index Drill into nested arrays via dot-notation path (or var.path sugar)
replace Sequential str_replace pairs
json_encode Encode value as JSON, optional flags
json_decode Decode JSON string
sp2nbsp Convert runs of spaces to non-breaking spaces
spaceless Strip whitespace between HTML tags (preserves <pre>, <textarea>)
regex preg_replace; strict mode returns null on no match
trim trim with custom character mask
url_encode URL-encode a scalar or encode an array as query string
striptags Strip HTML tags with optional allow-list
nl2br Convert double newlines to <p> blocks
truncate Truncate to a length with ellipsis
split Explode or str_split depending on delimiter
list Pick one of N arguments by index
enclose Wrap with prefix and postfix strings
check Render a checkbox <input> value/checked pair
checkbox Render checked attribute if truthy
select Render selected attribute if matches
pre Mark next output as preformatted (no auto-indent)

API reference

Templatest::read(string $filename, ?string $cache_dir = null): TemplatestUser

Compiles (or loads from cache) the template and returns a renderable instance.

  • $filename — path to the template file
  • $cache_dir — optional directory for cached compiled templates
$tpl = Templatest::read('page.tpl', __DIR__ . '/cache');

TemplatestUser

Magic __set

$tpl->name = 'Alice';              // set top-level variable
$tpl->user_name = 'Alice';         // underscore-split: 'user' → 'name'

The single-segment form ($tpl->name = ...) sets a top-level variable. The underscore form walks the pattern tree, so $tpl->items_item_text is equivalent to $tpl->set('items', 'item', 'text', value).

Magic __get

$count = (int) $tpl->name;         // number of times 'name' was set

Returns the set-count for the chroot-prefixed variable name. Returns int, not an object — chained property access ($tpl->items->item->text) does NOT work; use the underscore-split form for both reading and writing.

Chroot context

$tpl->enter('user_');              // push context — variable names get 'user_' prefix
$tpl->name = 'Alice';              // → 'user_name'
$tpl->age  = 30;                   // → 'user_age'
$tpl->leave();                     // pop context

Pull functions

$tpl->pull('csrf', function (bool $is_block, string $loc) {
    return '<input type="hidden" name="_token" value="' . bin2hex(random_bytes(16)) . '">';
});

Clone by pattern name

$header = $tpl->header;           // clones the template rooted at pattern 'header'
$header->title = 'Welcome';
echo $header;                     // renders only the header pattern

Direct invocation

$tpl('name', 'Alice', 'age', 30);  // equivalent to $tpl->set('name', 'Alice', 'age', 30)

set()

$tpl->set('name', 'Alice');                        // underscore-split path
$tpl->set(['name' => 'Alice', 'age' => 30]);       // array form
$tpl->set('user', 'name', 'Alice');                // explicit nested path

TemplatestEscaper

Context-aware escaping utilities, exposed via the escape filter:

$escaper = new TemplatestEscaper();
echo $escaper->escape_html('<b>x</b>');    // &lt;b&gt;x&lt;/b&gt;
echo $escaper->escape_js("'");             // \x27
echo $escaper->escape_url('hello world');  // hello%20world

Caching

Compiled templates are cached to disk if a $cache_dir is provided. The cache is invalidated when:

  • the source file's mtime changes
  • the cache file is missing or unreadable
  • the cache file's CODE_VERSION doesn't match the running version

To disable caching for a single file, set its write bit off (chmod 644); Templatest will re-read from source each time.

To clear the entire cache:

rm -rf /path/to/cache/*

Configuration

Setup directives (in .tpl files)

.escape html                  # default escape mode (default: raw)
.escape html pattern_name     # escape mode for a specific pattern
.patch main title "Welcome"   # pre-set variable 'title' to "Welcome" in pattern 'main'
.include header.tpl           # include another template file

Default escape modes

TemplatestSetter::ESCAPER constant controls the default when a pattern has no explicit .escape directive. Default is 'raw' (no escaping).

Testing

composer install
vendor/bin/phpunit

Run with testdox formatting:

vendor/bin/phpunit --testdox

Run a single test:

vendor/bin/phpunit --filter test_random_token

Generate coverage (requires pcov or xdebug):

composer require --dev pcov/clobber
vendor/bin/phpunit --coverage-html coverage/

Architecture

src/
├── Templatest.php         # compiler entry point; reads & caches templates
├── TemplatestUser.php     # public API; per-instance renderable
├── TemplatestSetter.php   # assigns values into chunks; runs filter pipes
├── TemplatestFilters.php  # built-in filter registry
├── TemplatestEscaper.php  # context-aware string escaping (Zend-derived)
├── Helper.php             # standalone utilities (path, serialize, stringify, etc.)
└── Exception/
    ├── InvalidArgumentException.php
    └── RuntimeException.php

Compilation pipeline

.tpl source
   │
   ▼
parse_file()         ─── reads file, splits into patterns, processes .include / .escape / .patch
   │
   ▼
inline_definitions() ─── extracts inline [= abc def = ... =] blocks
   │
   ▼
compile()            ─── scans each pattern for [' ... '] tags,
                        builds chunks + var/sub/pull metadata
   │
   ▼
inline_static_subs() ─── inlines subpatterns that have no dynamic content
   │
   ▼
inline_defaults()    ─── applies [' x | default "..." '] defaults
   │
   ▼
cache → TemplatestUser instance

Render pipeline

$tpl = Templatest::read('page.tpl');
$tpl->title = 'Hello';
echo $tpl;    // __toString() → commit(root)
                  ├── iterate pull actions
                  ├── recurse into subpatterns
                  └── implode chunks

Standalone helper utilities (Helper class)

Methods used internally that may also be useful on their own:

  • Helper::join_path(...$parts): string|false
  • Helper::serialize_data($data, $options = 0): string
  • Helper::unserialize_data($text): mixed
  • Helper::http64_encode($data): string
  • Helper::is_empty($val): bool
  • Helper::random_token($length = 10, $complexity = 2): string
  • Helper::str_rand($min, $max): int
  • Helper::qencode($name, $value): string
  • Helper::stringify($x, $compact = 0, $full = 1): string|false
  • Helper::dbg(...$args): void
  • Helper::callee($class_filter): string

Credits

  • Original WackoWiki templating engine by the WackoWiki team
  • HTML escaper adapted from the BSD-licensed Zend Framework
  • Standalone port and namespace cleanup by the Templatest maintainers

License

BSD-3-Clause — see LICENSE file.

About

Templatest is a sophisticated, transition-style template engine for PHP

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages