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
"METHODS" -- render_file, render_string, add_filter, add_test
"TEMPLATE SYNTAX" -- variables, filters, tests, tags
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 thesafefilter to bypass:{{ html | safe }}. trim_blocks-
When true, the newline immediately following a
{% ... %}tag is removed. Equivalent to Jinja2'strim_blocks=True. lstrip_blocks-
When true, leading whitespace (spaces and tabs) before a
{% ... %}tag on a line is stripped. Equivalent to Jinja2'slstrip_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 thetemplate_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,lowerfilters operate on bytes, not characters, for Perl 5.5.3 compatibility. Usemb::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
.pmfile. No build step, no installation required beyond copying. - Zero dependencies
-
Only the core module
Carpis 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.