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.
- 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
- PHP 8.1 or later
iconvormbstringextension (for non-UTF-8 encoding support inTemplatestEscaper)
composer require wackowiki/templatestOr, if vendoring manually:
require __DIR__ . '/vendor/autoload.php';[= 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.)
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;<!DOCTYPE html>
<html>
<head><title>My Page</title></head>
<body>
<h1>Hello, World!</h1>
<ul>
<li>first</li>
<li>second</li>
</ul>
</body>
</html>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
Pattern blocks use [= name =] or [==== name ====] — any number of = signs works:
[= main =]
[==== main ====]
[================ main =================]
All three are equivalent. Use whichever reads best.
[= pattern_name =]
... template body ...
[=]
The first pattern defined in a file is the main pattern and is the one rendered by default.
[= anon = ...inline body... =]
Useful for small reusable chunks. The name may be a single punctuation character (anonymous) or an identifier.
[' var_name ']
[' var_name | filter1 | filter2 ']
Variables are assigned from PHP via $tpl->var_name = 'value' or via underscore-split paths.
[ ' sub_pattern ' ]
[ ' my_name sub_pattern ' ]
Embeds a pattern instance. Each invocation instantiates a fresh copy unless static inlining applies.
[ '' 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[' msg | upper | trim ']
[' msg | default "anonymous" | escape ']
Filters are evaluated left-to-right. The result of each filter is the input to the next.
.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
| 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) |
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');$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).
$count = (int) $tpl->name; // number of times 'name' was setReturns 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.
$tpl->enter('user_'); // push context — variable names get 'user_' prefix
$tpl->name = 'Alice'; // → 'user_name'
$tpl->age = 30; // → 'user_age'
$tpl->leave(); // pop context$tpl->pull('csrf', function (bool $is_block, string $loc) {
return '<input type="hidden" name="_token" value="' . bin2hex(random_bytes(16)) . '">';
});$header = $tpl->header; // clones the template rooted at pattern 'header'
$header->title = 'Welcome';
echo $header; // renders only the header pattern$tpl('name', 'Alice', 'age', 30); // equivalent to $tpl->set('name', 'Alice', 'age', 30)$tpl->set('name', 'Alice'); // underscore-split path
$tpl->set(['name' => 'Alice', 'age' => 30]); // array form
$tpl->set('user', 'name', 'Alice'); // explicit nested pathContext-aware escaping utilities, exposed via the escape filter:
$escaper = new TemplatestEscaper();
echo $escaper->escape_html('<b>x</b>'); // <b>x</b>
echo $escaper->escape_js("'"); // \x27
echo $escaper->escape_url('hello world'); // hello%20worldCompiled templates are cached to disk if a $cache_dir is provided. The cache is invalidated
when:
- the source file's
mtimechanges - the cache file is missing or unreadable
- the cache file's
CODE_VERSIONdoesn'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/*.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
TemplatestSetter::ESCAPER constant controls the default when a pattern has no explicit
.escape directive. Default is 'raw' (no escaping).
composer install
vendor/bin/phpunitRun with testdox formatting:
vendor/bin/phpunit --testdoxRun a single test:
vendor/bin/phpunit --filter test_random_tokenGenerate coverage (requires pcov or xdebug):
composer require --dev pcov/clobber
vendor/bin/phpunit --coverage-html coverage/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
.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
$tpl = Templatest::read('page.tpl');
$tpl->title = 'Hello';
echo $tpl; // __toString() → commit(root)
├── iterate pull actions
├── recurse into subpatterns
└── implode chunks
Methods used internally that may also be useful on their own:
Helper::join_path(...$parts): string|falseHelper::serialize_data($data, $options = 0): stringHelper::unserialize_data($text): mixedHelper::http64_encode($data): stringHelper::is_empty($val): boolHelper::random_token($length = 10, $complexity = 2): stringHelper::str_rand($min, $max): intHelper::qencode($name, $value): stringHelper::stringify($x, $compact = 0, $full = 1): string|falseHelper::dbg(...$args): voidHelper::callee($class_filter): string
- 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
BSD-3-Clause — see LICENSE file.