NAME
Text::Stencil - fast XS list/table renderer with escaping, formatting, and transform chaining
SYNOPSIS
use Text::Stencil;
my $s = Text::Stencil->new(
header => '<table><tr><th>id</th><th>name</th></tr>',
row => '<tr><td>{0:int}</td><td>{1:html}</td></tr>',
footer => '</table>',
);
my $html = $s->render(\@rows);
# hashrefs, chaining, separator
my $s = Text::Stencil->new(
header => '<ul>',
row => '<li>{title:default:Untitled|trim|trunc:80|html}</li>',
footer => '</ul>',
separator => "\n",
);
# single row, stream to file
print $s->render_one({id => 1, title => 'Hello'});
$s->render_to_fh($fh, \@rows);
DESCRIPTION
Renders lists of uniform data (arrayrefs or hashrefs) into text output using a pre-compiled row template. The template is parsed once at construction; rendering is a tight C loop with direct buffer writes and zero Perl interpretation overhead.
2-3x faster than Text::Xslate for table/list rendering.
The template is parsed once; rendering is a tight C loop. Not safe for concurrent renders from multiple threads (see THREAD SAFETY).
CONSTRUCTOR
new
my $s = Text::Stencil->new(%opts);
Options:
header- string prepended before all rows (default: empty)row- row template with{field:type}placeholders (required)separator- string inserted between rows (default: none)escape_char- delimiter character instead of{(default:{). Paired closing:[→],(→),<→>, others use the same char for open and close. Useful for JSON templates where literal braces are needed.skip_if- column index or field name. Rows where this field is truthy (non-empty, not"0", not undef) are skipped.skip_unless- column index or field name. Rows where this field is not truthy are skipped.
As a shorthand, new may be called with a single string argument, which is used as the row template: Text::Stencil->new($template) is equivalent to Text::Stencil->new(row => $template).
from_file
my $s = Text::Stencil->from_file('template.tpl', separator => "\n");
Load template from a file. The file can use section markers:
__HEADER__
<table>
__ROW__
<tr><td>{0:html}</td></tr>
__FOOTER__
</table>
Without markers, the entire file content is used as the row template.
clone
my $s2 = $s->clone(row => '{0:uc}');
Create a new renderer reusing the original's header/footer.
METHODS
render
my $output = $s->render(\@rows);
Render all rows. Returns a UTF-8 string.
render_one
my $output = $s->render_one(\@row);
my $output = $s->render_one(\%row);
Render a single row without wrapping in an arrayref.
render_sorted
my $output = $s->render_sorted(\@rows, $sort_by);
my $output = $s->render_sorted(\@rows, $sort_by, {descending => 1, numeric => 1});
Render rows sorted by a field. $sort_by is a column index for arrayref rows or a field name for hashref rows. A leading - on the field name sorts descending: '-score'. It can also be an arrayref for multi-column sort: [0, 1] or ['name', 'age']. Sorts lexically ascending by default. Optional third argument is a hashref: descending reverses order, numeric compares numerically.
render_to_fh
$s->render_to_fh($fh, \@rows);
Render directly to a filehandle, flushing in 64KB chunks.
render_cb
my $output = $s->render_cb(sub { return \@row_or_undef });
$s->render_cb(sub { return \@row_or_undef }, $fh);
Callback-based rendering. The callback is called repeatedly and should return an arrayref or hashref (one row) or undef to stop. If a filehandle is given, output is streamed to it; otherwise returns a string.
columns
my $cols = $s->columns; # [0, 2] or ['name', 'id']
Returns field references used in the row template.
row_count
$s->render(\@rows);
my $n = $s->row_count;
Number of rows processed by the last render().
TEMPLATE SYNTAX
Field references
{0}, {1} for arrayref rows. {name}, {id} for hashref rows. Mode auto-detected from the template. Negative indices count from the end: {-1} is the last element, {-2} the second-to-last, etc.
{#} is the current row number (0-based). Works with chaining: {#:int_comma}, {#:pad:4}. In render_one, the row number is 0.
Literal delimiters
{{ produces a literal { in output. Useful for JSON templates:
{{"id":{0:int}} # produces {"id":42}
Works with any escape_char: [[ produces [ when using escape_char => '['.
Types
Escaping / encoding
html, html_br, url, json, hex, base64, base64url, raw
Numeric
int, int_comma, float:N, sprintf:FMT
String transforms
trim, uc, lc, pad:N, rpad:N, trunc:N, substr:S:L, replace:OLD:NEW, mask:N, length
Logic / conversion
default:VALUE, bool:TRUTHY:FALSY, if:TEXT, unless:TEXT, map:K1=V1:K2=V2:*=DEFAULT, wrap:PREFIX:SUFFIX
Data formatting
count, date:FMT, plural:SINGULAR:PLURAL, number_si, bytes_si, elapsed, ago, coalesce:FIELD1:FIELD2:DEFAULT - use the primary field if non-empty (unlike bool/if, the string "0" counts as present), otherwise try each fallback field in order; the last parameter is a literal default string
Chaining
{0:trim|trunc:80|html} # pipe transforms left to right
count and coalesce act on the raw field value (a container's size, or the first truthy field), so each must be the first transform in its chain. Using either later in a chain is a compile-time error.
UNICODE
UTF-8 transparent. All string operations preserve multi-byte sequences. Output is flagged UTF-8. uc/lc are ASCII-only.
THREAD SAFETY
The object is not safe for concurrent renders from multiple threads due to shared render buffer and last_row_count state. Create separate objects per thread, or serialize access. render_sorted additionally uses process-global sort state, so it must not be called concurrently even on separate objects; serialize calls to it.
PERFORMANCE
Perl 5.40, x86_64 Linux.
HTML table (13 rows, html escape):
Rate Text::Xslate hashref chained arrayref render_one
Text::Xslate 413K/s -- -44% -49% -55% -92%
render hashref 733K/s 77% -- -10% -21% -86%
render chained 813K/s 97% 11% -- -12% -84%
render arrayref 922K/s 123% 26% 13% -- -82%
render_one 5161K/s 1150% 604% 534% 460% --
Transform throughput (1000 rows, single transform):
default:x 67.4K/s int 52.4K/s int_comma 50.1K/s
trunc:20 44.4K/s raw 39.8K/s json 33.7K/s
uc 36.4K/s url 32.2K/s html 28.7K/s
trim|html 23.6K/s float:2 6.3K/s
Chain depth scaling (1000 rows):
1 (html) 19.1K/s
2 (trim|html) 15.8K/s (-17%)
3 (trim|uc|html) 11.2K/s (-29%)
4 (trim|uc|trunc:20|html) 11.0K/s (-1%)
Row count scaling (int + html escape per row):
~25M rows/s constant from 10 to 10000 rows
render vs render_one (single row):
render_one 7.0M/s (44% faster than render for single rows)
Run perl bench.pl for your own numbers.
AUTHOR
vividsnow
LICENSE
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.