wackowiki / templatest
Push-style, lazy, topo-ordered incremental template engine for WackoWiki
Requires
- php: >=8.3
Requires (Dev)
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-09-09 14:04:47 UTC
README
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
iconvormbstringextension (for non-UTF-8 encoding support inTemplatestEscaper)
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>'); // <b>x</b> 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
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/*
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|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
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.