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.
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 truthy, 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
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.
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.