asko/toretto

A simple and extendable templating library.

v1.1 2024-12-21 20:16 UTC

This package is not auto-updated.

Last update: 2025-01-04 20:36:41 UTC


README

builds.sr.ht status coverage

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 the if attributes.
  • Asko\Toretto\AttributeParsers\UnlessAttributeParser - Parser the unless attributes.
  • Asko\Toretto\AttributeParsers\InnerPartialAttributeParser - Parser the inner-partial attributes.
  • Asko\Toretto\AttributeParsers\InnerHtmlAttributeParser - Parser the inner-html attributes.
  • Asko\Toretto\AttributeParsers\InnerTextAttributeParser - Parser the inner-text attributes.
  • Asko\Toretto\AttributeParsers\OuterPartialAttributeParser - Parser the outer-partial attributes.
  • Asko\Toretto\AttributeParsers\OuterHtmlAttributeParser - Parser the outer-html attributes.
  • Asko\Toretto\AttributeParsers\OuterTextAttributeParser - Parser the outer-text attributes.
  • Asko\Toretto\AttributeParsers\ForeachAttributeParser - Parses the foreach 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.