asko / toretto
A simple and extendable templating library.
Requires (Dev)
- phpunit/phpunit: ^11
This package is not auto-updated.
Last update: 2025-01-04 20:36:41 UTC
README
A simple and extendable templating library built on top of the new \DOM\HTMLDocument
of PHP 8.4.
Features
- Simple syntax: Toretto is a superset of HTML, meaning that any valid HTML is also valid Toretto.
- Good editor support: Because Toretto is a superset of HTML, it works in all editors without additional plugins.
- Interpolation: You can interpolate values from a data array into your templates.
- Modifiers: You can modify the interpolated values using modifiers.
- Conditionals: You can show or hide blocks using simple (soon: complex) expressions.
- Partials: You can include other templates inside your templates.
- Loops: You can loop over iterable data.
- Extendable: You can implement custom attribute parsers and expression modifiers.
Example syntax
<!DOCTYPE html>
<html>
<head>
<title inner-text="{title}"></title>
</head>
<body>
<h1 inner-text="{title}"></h1>
<div class="posts" if="posts">
<div foreach="posts as post">
<h2 class="post-title">
<a :href="/blog/{post.url}" inner-text="{post.title | capitalize}"></a>
</h2>
<div class="post-date" inner-text="{post.date | date:yyyy-MM-dd}"></div>
<div class="post-content" inner-html="{post.body}"></div>
</div>
</div>
</body>
</html>
Installation
composer require asko/toretto
Usage
A simple example of how to use Toretto with default configuration looks like this:
use Asko\Toretto;
$toretto = new Toretto('<p inner-text="Hello {who}"></p>', ['who' => 'World']);
$html = $toretto->toHtml(); // <p>Hello World</p>
Attributes
Toretto works by parsing attributes in the template.
inner-text
Sets the inner text of the element to the value of the attribute.
Toretto template where title
key is Hello, World!
:
<h1 inner-text="{title}"></h1>
Results in:
<h1>Hello, World!</h1>
inner-html
Sets the inner HTML of the element to the value of the attribute.
Toretto template where content
key is <p>Hello, World!</p>
:
<div inner-html="{content}"></div>
Results in:
<div>
<p>Hello, World!</p>
</div>
inner-partial
Sets the inner HTML of the element to the value of the parsed Toretto template. Inherits all the same data as the parent template.
Toretto template with data such as:
$data = [
'title' => 'My Web Portal Thing',
'header' => '<div class="header"><h1 inner-text="title"></h1></div>'
];
And the template is:
<div inner-partial="header"></div>
Results in:
<div>
<div class="header">
<h1>My Web Portal Thing</h1>
</div>
</div>
outer-text
Sets the outer text of the element to the value of the attribute.
Toretto template where title
key is Hello, World!
:
<h1 outer-text="{title}"></h1>
Results in:
Hello, World!
outer-html
Sets the outer HTML of the element to the value of the attribute.
Toretto template where content
key is <p>Hello, World!</p>
:
<div outer-html="{content}"></div>
Results in:
<p>Hello, World!</p>
outer-partial
Sets the outer HTML of the element to the value of the parsed Toretto template. Inherits all the same data as the parent template.
Toretto template with data such as:
$data = [
'title' => 'My Web Portal Thing',
'header' => '<div class="header"><h1 inner-text="title"></h1></div>'
];
And the template is:
<div outer-partial="header"></div>
Results in:
<div class="header">
<h1>My Web Portal Thing</h1>
</div>
if
Removes the element if the attribute is false-y.
Toretto template where show
key is false
:
<div if="show">Hello, World!</div>
Results in:
<!-- Empty -->
unless
Removes the element if the attribute is truthy.
Htmt template where hide
key is true
:
<div unless="hide">Hello, World!</div>
Results in:
<!-- Empty -->
foreach
Loops anything iterable.
For example, to loop over a collection of posts
and then use post
as the variable of each iteration, you can do something
like this:
<div foreach="posts as post">
<h2 inner-text="post.title"></h2>
</div>
If you do not care about using any of the iteration data, you can also entirely omit as ...
from the iterative expression,
like so:
<div foreach="posts">
...
</div>
And, you can also assign the key of the iteration to a variable, like so:
<div foreach="posts as index:post">
<h2 :class="post-{post.index}" inner-text="post.title"></h2>
</div>
:*
(Generic Value Attributes)
You can use the :*
attribute to set any attribute on an element to the interpolated value of the generic value attribute.
For example, to set the href
attribute of an element, you can use the :href
attribute:
<a :href="/blog/{slug}">Hello, World!</a>
Results in:
<a href="/blog/hello-world">Hello, World!</a>
If the slug
key is hello-world
.
Modifiers
All interpolated values in expressions can be modified using modifiers. Modifiers are applied to the value of the attribute, and they can be chained, like so:
<h1 inner-text="{title | uppercase | reverse}"></h1>
Note that if you have nothing other than the interpolated variable in the attribute, then you can omit the curly brackets, and so this would also work:
<h1 inner-text="title | uppercase | reverse"></h1>
date
Parses the value into a formatted date string.
<p inner-text="published_at | date:YYYY-mm-dd"></p>
truncate
Truncates the value to the specified length.
<p inner-text="{title | truncate:10}"></p>
This also works on collections, so you can use truncate
to limit items in an array as well.
Extending
Attribute Parsers
You can add (or replace) attribute parsers in Toretto by adding them to the $attributeParsers
array,
when creating a new instance of the Toretto
class, like so:
use Asko\Toretto;
use Asko\Toretto\AttributeParsers;
$toretto = new Toretto('<p inner-text="Hello {who}"></p>', ['who' => 'World']);
$toretto->attributeParsers = [
new InnerTextAttributeParser(),
...
];
$html = $toretto->toHtml(); // <p>Hello World</p>
A custom attribute parser must extend the BaseAttributeParser
class, like so:
use Asko\Toretto\Core\Attributes\Query;
use Dom\Node;
use Dom\NodeList;
#[Query('//*[@inner-text]')]
class InnerTextAttributeParser extends BaseAttributeParser
{
/**
* @param NodeList<Node> $nodeList
* @return void
*/
#[\Override]
public function parse(NodeList &$nodeList): void
{
foreach($nodeList as $node) {
$parsedExpression = $this->parseExpression($node->getAttribute('inner-text'), serialize: true);
$node->textContent = $parsedExpression;
$node->removeAttribute('inner-text');
}
}
}
All attributes are matched via XTag queries, and you can match your attribute parser class with a XTag query by using the
Query
attribute.
List of built-in attribute parsers
Asko\Toretto\AttributeParsers\GenericValueAttributeParser
- Parser the:*
attributes.Asko\Toretto\AttributeParsers\IfAttributeParser
- Parser theif
attributes.Asko\Toretto\AttributeParsers\UnlessAttributeParser
- Parser theunless
attributes.Asko\Toretto\AttributeParsers\InnerPartialAttributeParser
- Parser theinner-partial
attributes.Asko\Toretto\AttributeParsers\InnerHtmlAttributeParser
- Parser theinner-html
attributes.Asko\Toretto\AttributeParsers\InnerTextAttributeParser
- Parser theinner-text
attributes.Asko\Toretto\AttributeParsers\OuterPartialAttributeParser
- Parser theouter-partial
attributes.Asko\Toretto\AttributeParsers\OuterHtmlAttributeParser
- Parser theouter-html
attributes.Asko\Toretto\AttributeParsers\OuterTextAttributeParser
- Parser theouter-text
attributes.Asko\Toretto\AttributeParsers\ForeachAttributeParser
- Parses theforeach
attributes.
Expression Modifiers
You can add (or replace) expression modifiers in Toretto by adding them to the $expressionModiifers
array,
when creating a new instance of the Toretto
class, like so:
use Asko\Toretto;
use Asko\Toretto\ExpressionModifiers;
$toretto = new Toretto('<p inner-text="Hello {who}"></p>', ['who' => 'World']);
$toretto->expressionModifiers = [
new TruncateExpressionModifier(),
...
];
$html = $toretto->toHtml(); // <p>Hello World</p>
A custom expression attribute must implement the ExpressionModifier
interface, like so:
<?php
use Asko\Toretto\Core\Attributes\Name;
use Asko\Toretto\ExpressionModifier;
#[Name('my-custom-modifier')]
class MyCustomExpressionModifier implements ExpressionModifier
{
/**
* @param mixed $value The value to be modified.
* @param List<string>|null $opts Optional parameter.
* @return mixed The modified expression.
*/
public static function modify(mixed $value, ?array $opts = null): mixed
{
// Do something here.
}
}
All expression modifiers need to have a name, and you can name your modifier with the Name
attribute.
List of built-in expression modifiers
Asko\Toretto\ExpressionModifiers\TruncateExpressionModiifer
- Truncates the value (both strings and collections).Asko\Toretto\ExpressionModifiers\DateExpressionModiifer
- Parses the value into a formatted date string.