NAME

HP::Handy - A tiny Jinja2-compatible template engine for Perl 5.5.3 and later

VERSION

Version 0.01

SYNOPSIS

use HP::Handy;

my $tmpl = HP::Handy->new(template_dir => './templates');

# Render from file
my $html = $tmpl->render_file('index.html', { name => 'World', items => [1,2,3] });

# Render from string
my $out = $tmpl->render_string('Hello, {{ name }}!', { name => 'World' });

TABLE OF CONTENTS

DESCRIPTION

HP::Handy is a single-file, zero-dependency Jinja2-compatible template engine for Perl. It is designed for generating HTML pages from HTTP::Handy applications.

The goals of the project are simplicity and portability. The entire implementation fits in one file with no installation step.

HP stands for HomePage. Together with HTTP::Handy (server) and DB::Handy (database), HP::Handy completes the three-layer stack for building local web applications in pure Perl.

REQUIREMENTS

Perl     : 5.5.3 or later -- all versions, all platforms
OS       : Any (Windows, Unix, macOS, and others)
Modules  : Core only -- Carp
Model    : Pure Perl, regex-based evaluator

No CPAN modules are required.

CONSTRUCTOR

new(%args)

Creates a new template engine instance.

my $tmpl = HP::Handy->new(
    template_dir  => './templates',  # default: '.'
    auto_escape   => 1,              # default: 1 (HTML-escape {{ }} output)
    trim_blocks   => 0,              # default: 0 (remove newline after {% %})
    lstrip_blocks => 0,              # default: 0 (strip leading whitespace before {% %})
);
template_dir

Directory to search for template files used by render_file() and {% include %} / {% extends %} tags.

auto_escape

When true (the default), all {{ expr }} output is HTML-escaped. Use the safe filter to bypass: {{ html | safe }}.

trim_blocks

When true, the newline immediately following a {% ... %} tag is removed. Equivalent to Jinja2's trim_blocks=True.

lstrip_blocks

When true, leading whitespace (spaces and tabs) before a {% ... %} tag on a line is stripped. Equivalent to Jinja2's lstrip_blocks=True.

block_start / block_end

Opening and closing delimiters for control tags. Defaults: {% and %}.

var_start / var_end

Opening and closing delimiters for variable output. Defaults: {{ and }}.

comment_start / comment_end

Opening and closing delimiters for comments. Defaults: {# and #}.

METHODS

render_file($filename, \%vars)

Load and render a template file. $filename is relative to template_dir.

my $html = $tmpl->render_file('index.html', { name => 'World' });

render_string($source, \%vars)

Render a template given as a string.

my $out = $tmpl->render_string('Hello, {{ name }}!', { name => 'World' });

add_filter($name, \&code)

Register a custom filter function. The first argument to the function is the value being filtered; subsequent arguments come from the template call.

$tmpl->add_filter('rot13', sub {
    my $s = $_[0];
    $s =~ tr/A-Za-z/N-ZA-Mn-za-m/;
    $s
});

# In template: {{ text | rot13 }}

add_test($name, \&code)

Register a custom test function. Returns a true/false value.

$tmpl->add_test('palindrome', sub {
    my $s = $_[0];
    defined $s && $s eq scalar reverse $s
});

# In template: {% if word is palindrome %}...{% endif %}

TEMPLATE SYNTAX

Delimiters

{{ expr }}          Variable / expression output
{% tag %}           Control tag (if, for, set, include, ...)
{# comment #}       Comment (removed from output)

Whitespace control: add a dash inside the delimiter to strip whitespace.

{%- tag -%}         Strip whitespace before and after the tag

Variables

{{ name }}
{{ user.email }}
{{ items[0] }}
{{ config["debug"] }}

Filters

{{ name | upper }}
{{ text | truncate(80) }}
{{ text | replace("old", "new") }}
{{ value | default("n/a") }}
{{ items | join(", ") }}

Tests

{% if x is defined %}
{% if n is odd %}
{% if x is divisibleby 3 %}
{% if x is not none %}

Conditional

{% if condition %}
    ...
{% elif other %}
    ...
{% else %}
    ...
{% endif %}

Inline conditional:

{{ "yes" if flag else "no" }}

For Loop

{% for item in list %}
    {{ loop.index }}: {{ item }}
{% endfor %}

{% for item in list if item != '' %}
    {{ item }}
{% else %}
    (empty)
{% endfor %}

Loop variable loop:

loop.index      1-based counter
loop.index0     0-based counter
loop.revindex   reverse counter (1-based)
loop.revindex0  reverse counter (0-based)
loop.first      true on first iteration
loop.last       true on last iteration
loop.length     total number of items
loop.odd        true on odd iterations
loop.even       true on even iterations

Dict iteration:

{% for key, value in mapping %}
    {{ key }}: {{ value }}
{% endfor %}

Set

{% set x = 42 %}
{% set greeting = "Hello, " ~ name %}

Include

{% include "header.html" %}
{% include "optional.html" ignore missing %}

Raw

{% raw %}
    {{ this is not rendered }}
{% endraw %}

With

{% with x = 10, y = 20 %}
    {{ x + y }}
{% endwith %}

BUILT-IN FILTERS

upper           Convert to uppercase
lower           Convert to lowercase
trim            Strip leading/trailing whitespace
length          String or list length
reverse         Reverse string or list
escape / e      HTML-escape (&, <, >, ", ')
safe            Mark as safe (skip auto_escape)
default / d     Return value or default if undefined/empty
replace         Replace substring
truncate        Truncate string
join            Join list with separator
first           First element of list
last            Last element of list
list            Wrap scalar in list
abs             Absolute value
int             Convert to integer
float           Convert to float
string          Convert to string
title           Title-case each word
capitalize      Capitalize first letter
urlencode       Percent-encode URL
wordcount       Count words
batch           Split list into chunks of N
slice           Split list into N slices
sort            Sort list (optionally by attribute)
unique          Remove duplicates
min             Minimum value in list
max             Maximum value in list
sum             Sum of list
map             Map attribute over list
select          Filter list by truthy attribute
reject          Filter list by falsy attribute
count           Number of elements
nl2br           Replace newlines with <br>
striptags       Strip HTML tags
format          sprintf formatting
center          Center string in a field
indent          Indent lines
xmlattr         Render hash as XML attributes
tojson          Serialize to JSON
pprint          Pretty-print value as JSON (no external dependency)
forceescape     Force HTML-escape (ignores safe)

BUILT-IN TESTS

defined         Value is defined
none            Value is undef
string          Value is a string (not reference)
number          Value is numeric
sequence        Value is an array reference
mapping         Value is a hash reference
iterable        Value is array or hash reference
callable        Value is a code reference
odd             Integer is odd
even            Integer is even
divisibleby N   Integer is divisible by N
upper           String is all uppercase
lower           String is all lowercase
equalto X       Value equals X
ne X            Value does not equal X
in container    Value is in list/hash/string
lt / le / gt / ge   Numeric comparisons

TEMPLATE INHERITANCE

Child template:

{% extends "base.html" %}

{% block title %}My Page{% endblock %}

{% block content %}
<p>Hello from child.</p>
{% endblock %}

Base template (base.html):

<!DOCTYPE html>
<html>
<head><title>{% block title %}Default{% endblock %}</title></head>
<body>{% block content %}{% endblock %}</body>
</html>

MACROS

{% macro input(name, value="", type="text") %}
    <input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}

{{ input("username") }}
{{ input("password", type="password") }}

DIAGNOSTICS

add_filter: name required

add_filter() was called without a filter name argument.

add_filter: code must be coderef

The second argument to add_filter() is not a code reference.

add_test: name required

add_test() was called without a test name argument.

add_test: code must be coderef

The second argument to add_test() is not a code reference.

HP::Handy: path traversal not allowed in '$filename'

The template filename passed to render_file() or {% include %} contains .. which would allow reading files outside the template_dir.

HP::Handy: cannot open '$path': $!

render_file() could not open the template file. Check that the file exists and is readable.

LIMITATIONS

  • No compiled template cache. Templates are parsed on every render call. For high-throughput use, cache rendered strings at the application layer.

  • No recursive macros (calling a macro from within itself).

  • No import ({% from "macros.html" import input %}). Use {% include %} and call macros defined there.

  • No super() for accessing parent block content in template inheritance.

  • No namespace() for loop variable write-back across loop boundaries.

  • Nested dict/list literals in expressions have limited support. Complex data structures should be passed as Perl variables.

  • Expression parser is regex-based, not a full AST. Pathological expressions with many nested operators may not parse correctly. Prefer passing pre-computed values from Perl for complex logic.

  • No Unicode-aware string operations. The length, upper, lower filters operate on bytes, not characters, for Perl 5.5.3 compatibility. Use mb:: functions in your Perl code and pass pre-processed values to the template.

DESIGN PHILOSOPHY

HP::Handy adheres to the Perl 5.005_03 specification for the same reasons as HTTP::Handy and DB::Handy: simplicity, portability, and zero dependencies.

One file

The entire engine fits in one .pm file. No build step, no installation required beyond copying.

Zero dependencies

Only the core module Carp is used.

Jinja2 syntax

Uses the same {{ }} {% %} {# #} delimiters as Jinja2 so that HTML templates written for Jinja2 (Python), Twig (PHP), or Nunjucks (JavaScript) work with minimal changes.

Designed for HTTP::Handy

HP::Handy is the view layer. DB::Handy is the model layer. HTTP::Handy is the controller layer. Together they form a self-contained MVC web stack for Perl 5.5.3 and later.

SEE ALSO

HTTP::Handy -- the HTTP/1.0 server layer (same distribution family).

DB::Handy -- the flat-file database layer (same distribution family).

Jinja2 documentation (Python reference implementation): https://jinja.palletsprojects.com/

Template -- Template Toolkit, the full-featured Perl template engine. Requires Perl 5.8+. HP::Handy is the minimal alternative for Perl 5.5.3+.

AUTHOR

INABA Hitoshi <ina@cpan.org>

COPYRIGHT AND LICENSE

This software is free software; you can redistribute it and/or modify it under the same terms as Perl itself.