lib/Lemplate.pm - lemplate

Data types defined

Source code

  1. # ToDo:
  2. # - Use TT:Simple in Makefiles

  3. # ABSTRACT: compiles Perl TT2 templates to standalone Lua modules for OpenResty

  4. package Lemplate;
  5. use strict;
  6. use warnings;
  7. use Template 2.14;
  8. use Getopt::Long;

  9. # VERSION

  10. use Lemplate::Parser;

  11. #-------------------------------------------------------------------------------

  12. our %ExtraTemplates;
  13. our %ProcessedTemplates;
  14. our $TemplateName;

  15. sub usage {
  16.     <<'...';
  17. Usage:

  18.     lemplate --runtime [runtime-opt]

  19.     lemplate --compile [compile-opt] <template-list>

  20.     lemplate --runtime [runtime-opt] --compile [compile-opt] <template-list>

  21.     lemplate --list <template-list>

  22. Where "--runtime" and "runtime-opt" can include:

  23.     --runtime           Equivalent to --ajax=ilinsky --json=json2
  24.     --runtime=standard

  25.     --runtime=lite      Same as --ajax=none --json=none
  26.     --runtime=jquery    Same as --ajax=jquery --json=none
  27.     --runtime=yui       Same as --ajax=yui --json=yui
  28.     --runtime=legacy    Same as --ajax=gregory --json=json2

  29.     --json              By itself, equivalent to --json=json2
  30.     --json=json2        Include http://www.json.org/json2.js for parsing/stringifying
  31.     --json=yui          Use YUI: YAHOO.lang.JSON (requires external YUI)
  32.     --json=none         Doesn't provide any JSON functionality except a warning

  33.     --ajax              By itself, equivalent to --ajax=xhr
  34.     --ajax=jquery       Use jQuery for Ajax get and post (requires external jQuery)
  35.     --ajax=yui          Use YUI: yui/connection/connection.js (requires external YUI)
  36.     --ajax=xhr          Use XMLHttpRequest (will automatically use --xhr=ilinsky if --xhr is not set)
  37.     --ajax=none         Doesn't provide any Ajax functionality except a warning

  38.     --xhr               By itself, equivalent to --xhr=ilinsky
  39.     --xhr=ilinsky       Include http://code.google.com/p/xmlhttprequest/
  40.     --xhr=gregory       Include http://www.scss.com.au/family/andrew/webdesign/xmlhttprequest/

  41.     --xxx               Include XXX and JJJ helper functions

  42.     --compact           Use the YUICompressor compacted version of the runtime

  43. Where "compile-opt" can include:

  44.     --include_path=DIR  Add directory to INCLUDE_PATH

  45.     --start-tag
  46.     --end-tag
  47.     --pre-chomp
  48.     --post-chomp
  49.     --trim
  50.     --any-case
  51.     --eval
  52.     --noeval
  53.     -s, --source
  54.     --exclude

  55. For more information use:
  56.     perldoc lemplate
  57. ...
  58. }

  59. sub main {
  60.     my $class = shift;

  61.     my @argv = @_;

  62.     my ($template_options, $lemplate_options) = get_options(@argv);
  63.     my ($runtime, $compile, $list) = @$lemplate_options{qw/runtime compile list/};

  64.     if ($runtime) {
  65.         print runtime_source_code(@$lemplate_options{qw/runtime ajax json xhr xxx compact/});
  66.         return unless $compile;
  67.     }

  68.     my $templates = make_file_list($lemplate_options->{exclude}, @argv);
  69.     print_usage_and_exit() unless @$templates;

  70.     if ($list) {
  71.         foreach (@$templates) {
  72.             print STDOUT $_->{short} . "\n";
  73.         }
  74.         return;
  75.     }

  76.     if ($compile) {
  77.         my $lemplate = Lemplate->new(%$template_options);
  78.         print STDOUT $lemplate->_preamble;
  79.         for (my $i = 0; $i < @$templates; $i++) {
  80.             my $template = $templates->[$i];
  81.             #warn "processing $template->{short}";
  82.             my $content = slurp($template->{full});
  83.             if ($content) {
  84.                 %ExtraTemplates = ();
  85.                 print STDOUT $lemplate->compile_template_content(
  86.                     $content,
  87.                     $template->{short}
  88.                 );
  89.                 my @new_files;
  90.                 for my $new_template (keys %ExtraTemplates) {
  91.                     if (!$ProcessedTemplates{$new_template}) {
  92.                         if (!-f $new_template) {
  93.                             $new_template = "t/data/" . $new_template;
  94.                         }
  95.                         #warn $new_template;
  96.                         if (-f $new_template) {
  97.                             #warn "adding new template $new_template";
  98.                             push @new_files, $new_template;
  99.                         }
  100.                     }
  101.                 }
  102.                 push @$templates, @{ make_file_list({}, @new_files) };
  103.             }
  104.         }
  105.         print STDOUT "return _M\n";
  106.         return;
  107.     }

  108.     print_usage_and_exit();
  109. }

  110. sub get_options {
  111.     local @ARGV = @_;

  112.     my $runtime;
  113.     my $compile = 0;
  114.     my $list = 0;

  115.     my $start_tag = exists $ENV{LEMPLATE_START_TAG}
  116.         ? $ENV{LEMPLATE_START_TAG}
  117.         : undef;
  118.     my $end_tag = exists $ENV{LEMPLATE_END_TAG}
  119.         ? $ENV{LEMPLATE_END_TAG}
  120.         : undef;
  121.     my $pre_chomp = exists $ENV{LEMPLATE_PRE_CHOMP}
  122.         ? $ENV{LEMPLATE_PRE_CHOMP}
  123.         : undef;
  124.     my $post_chomp = exists $ENV{LEMPLATE_POST_CHOMP}
  125.         ? $ENV{LEMPLATE_POST_CHOMP}
  126.         : undef;
  127.     my $trim = exists $ENV{LEMPLATE_TRIM}
  128.         ? $ENV{LEMPLATE_TRIM}
  129.         : undef;
  130.     my $anycase = exists $ENV{LEMPLATE_ANYCASE}
  131.         ? $ENV{LEMPLATE_ANYCASE}
  132.         : undef;
  133.     my $eval_javascript = exists $ENV{LEMPLATE_EVAL_JAVASCRIPT}
  134.         ? $ENV{LEMPLATE_EVAL_JAVASCRIPT}
  135.         : 1;

  136.     my $source  = 0;
  137.     my $exclude = 0;
  138.     my ($ajax, $json, $xxx, $xhr, $compact, $minify);

  139.     my $help = 0;
  140.     my @include_paths;

  141.     GetOptions(
  142.         "compile|c"     => \$compile,
  143.         "list|l"        => \$list,
  144.         "runtime|r:s"   => \$runtime,

  145.         "start-tag=s"   => \$start_tag,
  146.         "end-tag=s"     => \$end_tag,
  147.         "trim=s"        => \$trim,
  148.         "pre-chomp"     => \$pre_chomp,
  149.         "post-chomp"    => \$post_chomp,
  150.         "any-case"      => \$anycase,
  151.         "eval!"         => \$eval_javascript,

  152.         "source|s"      => \$source,
  153.         "exclude=s"     => \$exclude,

  154.         "ajax:s"        => \$ajax,
  155.         "json:s"        => \$json,
  156.         "xxx"           => \$xxx,
  157.         "xhr:s"         => \$xhr,

  158.         "include_path"  => \@include_paths,
  159.         "compact"       => \$compact,
  160.         "minify:s"      => \$minify,

  161.         "help|?"        => \$help,
  162.     ) or print_usage_and_exit();

  163.     if ($help) {
  164.         print_usage_and_exit();
  165.     }

  166.     ($runtime, $ajax, $json, $xxx, $xhr, $minify) = map { defined $_ && ! length $_ ? 1 : $_ } ($runtime, $ajax, $json, $xxx, $xhr, $minify);
  167.     $runtime = "standard" if $runtime && $runtime eq 1;

  168.     print_usage_and_exit("Don't understand '--runtime $runtime'") if defined $runtime && ! grep { $runtime =~ m/$_/ } qw/standard lite jquery yui legacy/;
  169.     print_usage_and_exit("Can't specify --list with a --runtime and/or the --compile option") if $list && ($runtime || $compile);
  170.     print_usage_and_exit() unless $list || $runtime || $compile;

  171.     my $command =
  172.         $runtime ? 'runtime' :
  173.         $compile ? 'compile' :
  174.         $list ? 'list' :
  175.         print_usage_and_exit();

  176.     my $options = {};
  177.     $options->{START_TAG} = $start_tag if defined $start_tag;
  178.     $options->{END_TAG} = $end_tag if defined $end_tag;
  179.     $options->{PRE_CHOMP} = $pre_chomp if defined $pre_chomp;
  180.     $options->{POST_CHOMP} = $post_chomp if defined $post_chomp;
  181.     $options->{TRIM} = $trim if defined $trim;
  182.     $options->{ANYCASE} = $anycase if defined $anycase;
  183.     $options->{EVAL_JAVASCRIPT} = $eval_javascript if defined $eval_javascript;
  184.     $options->{INCLUDE_PATH} = \@include_paths;

  185.     return (
  186.         $options,
  187.         { compile => $compile, runtime => $runtime, list => $list,
  188.             source => $source,
  189.             exclude => $exclude,
  190.             ajax => $ajax, json => $json, xxx => $xxx, xhr => $xhr,
  191.             compact => $compact, minify => $minify },
  192.     );
  193. }


  194. sub slurp {
  195.     my $filepath = shift;
  196.     open(F, '<', $filepath) or die "Can't open '$filepath' for input:\n$!";
  197.     my $contents = do {local $/; <F>};
  198.     close(F);
  199.     return $contents;
  200. }

  201. sub recurse_dir {
  202.     require File::Find::Rule;

  203.     my $dir = shift;
  204.     my @files;
  205.     foreach ( File::Find::Rule->file->in( $dir ) ) {
  206.         if ( m{/\.[^\.]+} ) {} # Skip ".hidden" files or directories
  207.         else {
  208.             push @files, $_;
  209.         }
  210.     }
  211.     return @files;
  212. }

  213. sub make_file_list {
  214.     my ($exclude, @args) = @_;

  215.     my @list;

  216.     foreach my $arg (@args) {
  217.         unless (-e $arg) { next; } # file exists
  218.         unless (-s $arg or -d $arg) { next; } # file size > 0 or directory (for Win platform)
  219.         if ($exclude and $arg =~ m/$exclude/) { next; } # file matches exclude regex

  220.         if (-d $arg) {
  221.             foreach my $full ( recurse_dir($arg) ) {
  222.                 $full =~ /$arg(\/|)(.*)/;
  223.                 my $short = $2;
  224.                 push(@list, {full=>$full, short=>$short} );
  225.             }
  226.         }
  227.         else {
  228.             my $full = $arg;
  229.             my $short = $full;
  230.             $short =~ s/.*[\/\\]//;
  231.             push(@list, {full=>$arg, short=>$short} );
  232.         }
  233.     }

  234.     return [ sort { $a->{short} cmp $b->{short} } @list ];
  235. }

  236. sub print_usage_and_exit {
  237.     print STDOUT join "\n", "", @_, "Aborting!", "\n" if @_;
  238.     print STDOUT usage();
  239.     exit;
  240. }

  241. sub runtime_source_code {
  242.     require Lemplate::Runtime;
  243.     require Lemplate::Runtime::Compact;

  244.     unshift @_, "standard" unless @_;

  245.     my ($runtime, $ajax, $json, $xhr, $xxx, $compact) = map { defined $_ ? lc $_ : "" } @_[0 .. 5];

  246.     my $Lemplate_Runtime = $compact ? "Lemplate::Runtime::Compact" : "Lemplate::Runtime";

  247.     if ($runtime eq "standard") {
  248.         $ajax ||= "xhr";
  249.         $json ||= "json2";
  250.         $xhr ||= "ilinsky";
  251.     }
  252.     elsif ($runtime eq "jquery") {
  253.         $ajax ||= "jquery";
  254.     }
  255.     elsif ($runtime eq "yui") {
  256.         $ajax ||= "yui";
  257.         $json ||= "yui";
  258.     }
  259.     elsif ($runtime eq "legacy") {
  260.         $ajax ||= "xhr";
  261.         $json ||= "json2";
  262.         $xhr ||= "gregory";
  263.         $xxx = 1;
  264.     }
  265.     elsif ($runtime eq "lite") {
  266.     }

  267.     $ajax = "xhr" if $ajax eq 1;
  268.     $xhr ||= 1 if $ajax eq "xhr";
  269.     $json = "json2" if $json eq 1;
  270.     $xhr = "ilinsky" if $xhr eq 1;

  271.     my @runtime;

  272.     push @runtime, $Lemplate_Runtime->kernel if $runtime;

  273.     push @runtime, $Lemplate_Runtime->json2 if $json =~ m/^json2?$/i;

  274.     push @runtime, $Lemplate_Runtime->ajax_xhr if $ajax eq "xhr";
  275.     push @runtime, $Lemplate_Runtime->ajax_jquery if $ajax eq "jquery";
  276.     push @runtime, $Lemplate_Runtime->ajax_yui if $ajax eq "yui";

  277.     push @runtime, $Lemplate_Runtime->json_json2 if $json =~ m/^json2?$/i;
  278.     push @runtime, $Lemplate_Runtime->json_json2_internal if $json =~ m/^json2?[_-]?internal$/i;
  279.     push @runtime, $Lemplate_Runtime->json_yui if $json eq "yui";

  280.     push @runtime, $Lemplate_Runtime->xhr_ilinsky if $xhr eq "ilinsky";
  281.     push @runtime, $Lemplate_Runtime->xhr_gregory if $xhr eq "gregory";

  282.     push @runtime, $Lemplate_Runtime->xxx if $xxx;

  283.     return join ";", @runtime;
  284. }

  285. #-------------------------------------------------------------------------------

  286. sub new {
  287.     my $class = shift;
  288.     return bless { @_ }, $class;
  289. }

  290. sub compile_module {
  291.     my ($self, $module_path, $template_file_paths) = @_;
  292.     my $result = $self->compile_template_files(@$template_file_paths)
  293.       or return;
  294.     open MODULE, "> $module_path"
  295.         or die "Can't open '$module_path' for output:\n$!";
  296.     print MODULE $result;
  297.     close MODULE;
  298.     return 1;
  299. }

  300. sub compile_module_cached {
  301.     my ($self, $module_path, $template_file_paths) = @_;
  302.     my $m = -M $module_path;
  303.     return 0 unless grep { -M($_) < $m } @$template_file_paths;
  304.     return $self->compile_module($module_path, $template_file_paths);
  305. }

  306. sub compile_template_files {
  307.     my $self = shift;
  308.     my $output = $self->_preamble;
  309.     for my $filepath (@_) {
  310.         my $filename = $filepath;
  311.         $filename =~ s/.*[\/\\]//;
  312.         open FILE, $filepath
  313.           or die "Can't open '$filepath' for input:\n$!";
  314.         my $template_input = do {local $/; <FILE>};
  315.         close FILE;
  316.         $output .=
  317.             $self->compile_template_content($template_input, $filename);
  318.     }
  319.     return $output;
  320. }

  321. sub compile_template_content {
  322.     die "Invalid arguments in call to Lemplate->compile_template_content"
  323.       unless @_ == 3;
  324.     my ($self, $template_content, $template_name) = @_;
  325.     $TemplateName = $template_name;
  326.     my $parser = Lemplate::Parser->new( ref($self) ? %$self : () );
  327.     my $parse_tree = $parser->parse(
  328.         $template_content, {name => $template_name}
  329.     ) or die $parser->error;
  330.     my $output =
  331.         "-- $template_name\n" .
  332.         "template_map['$template_name'] = " .
  333.         $parse_tree->{BLOCK} .
  334.         "\n";
  335.     for my $function_name (sort keys %{$parse_tree->{DEFBLOCKS}}) {
  336.         my $name = "$template_name/$function_name";
  337.         next if $ProcessedTemplates{$name};
  338.         #warn "seen $name";
  339.         $ProcessedTemplates{$name} = 1;
  340.         $output .=
  341.             "template_map['$name'] = " .
  342.             $parse_tree->{DEFBLOCKS}{$function_name} .
  343.             "\n";
  344.     }
  345.     return $output;
  346. }

  347. sub _preamble {
  348.     return <<'...';
  349. --[[
  350.    This Lua code was generated by Lemplate, the Lua
  351.    Template Toolkit. Any changes made to this file will be lost the next
  352.    time the templates are compiled.

  353.    Copyright 2016 - Yichun Zhang (agentzh) - All rights reserved.

  354.    Copyright 2006-2014 - Ingy döt Net - All rights reserved.
  355. ]]

  356. local gsub = ngx.re.gsub
  357. local concat = table.concat
  358. local type = type
  359. local math_floor = math.floor
  360. local table_maxn = table.maxn

  361. local _M = {
  362.     version = '0.02'
  363. }

  364. local template_map = {}

  365. local function tt2_true(v)
  366.     return v and v ~= 0 and v ~= "" and v ~= '0'
  367. end

  368. local function tt2_not(v)
  369.     return not v or v == 0 or v == "" or v == '0'
  370. end

  371. local context_meta = {}

  372. function context_meta.plugin(context, name, args)
  373.     if name == "iterator" then
  374.         local list = args[1]
  375.         local count = table_maxn(list)
  376.         return { list = list, count = 1, max = count - 1, index = 0, size = count, first = true, last = false, prev = "" }
  377.     else
  378.         return error("unknown iterator: " .. name)
  379.     end
  380. end

  381. function context_meta.process(context, file)
  382.     local f = template_map[file]
  383.     if not f then
  384.         return error("file error - " .. file .. ": not found")
  385.     end
  386.     return f(context)
  387. end

  388. function context_meta.include(context, file)
  389.     local f = template_map[file]
  390.     if not f then
  391.         return error("file error - " .. file .. ": not found")
  392.     end
  393.     return f(context)
  394. end

  395. context_meta = { __index = context_meta }

  396. local function stash_get(stash, k)
  397.     local v
  398.     if type(k) == "table" then
  399.         v = stash
  400.         for i = 1, #k, 2 do
  401.             local key = k[i]
  402.             local typ = k[i + 1]
  403.             if type(typ) == "table" then
  404.                 local value = v[key]
  405.                 if type(value) == "function" then
  406.                     return value()
  407.                 end
  408.                 if value then
  409.                     return value
  410.                 end
  411.                 if key == "size" then
  412.                     if type(v) == "table" then
  413.                         return #v
  414.                     else
  415.                         return 1
  416.                     end
  417.                 else
  418.                     return error("virtual method " .. key .. " not supported")
  419.                 end
  420.             end
  421.             if type(key) == "number" and key == math_floor(key) and key >= 0 then
  422.                 key = key + 1
  423.             end
  424.             if type(v) ~= "table" then
  425.                 return nil
  426.             end
  427.             v = v[key]
  428.         end
  429.     else
  430.         v = stash[k]
  431.     end
  432.     if type(v) == "function" then
  433.         return v()
  434.     end
  435.     return v
  436. end

  437. local function stash_set(stash, k, v, default)
  438.     if default then
  439.         local old = stash[k]
  440.         if old == nil then
  441.             stash[k] = v
  442.         end
  443.     else
  444.         stash[k] = v
  445.     end
  446. end

  447. function _M.process(file, params)
  448.     local stash = params
  449.     local context = {
  450.         stash = stash,
  451.         filter = function (bits, name, params)
  452.             local s = concat(bits)
  453.             if name == "html" then
  454.                 s = gsub(s, "&", '&amp;', "jo")
  455.                 s = gsub(s, "<", '&lt;', "jo");
  456.                 s = gsub(s, ">", '&gt;', "jo");
  457.                 s = gsub(s, '"', '&quot;', "jo"); -- " end quote for emacs
  458.                 return s
  459.             end
  460.         end
  461.     }
  462.     context = setmetatable(context, context_meta)
  463.     local f = template_map[file]
  464.     if not f then
  465.         return error("file error - " .. file .. ": not found")
  466.     end
  467.     return f(context)
  468. end
  469. ...
  470. }

  471. 1;

  472. __END__

  473. =encoding utf8

  474. =head1 Name

  475. Lemplate - OpenResty/Lua template framework implementing Perl's TT2 templating language

  476. =head1 Status

  477. This is still under early development. Check back often.

  478. =head1 Synopsis

  479.     local templates = require "myapp.templates"
  480.     ngx.print(tempaltes.process("homepage.tt2", { var1 = 32, var2 = "foo" }))

  481. From the command-line:

  482.     lemplate --compile path/to/lemplate/directory/ > myapp/templates.lua

  483. =head1 Description

  484. Lemplate is a templating framework for OpenResty/Lua that is built over
  485. Perl's Template Toolkit (TT2).

  486. Lemplate parses TT2 templates using the TT2 Perl framework, but with a
  487. twist. Instead of compiling the templates into Perl code, it compiles
  488. them into Lua that can run on OpenResty.

  489. Lemplate then provides a Lua runtime module for processing
  490. the template code. Presto, we have full featured Lua
  491. templating language!

  492. Combined with OpenResty, Lemplate provides a really simple
  493. and powerful way to do web stuff.

  494. =head1 HowTo

  495. Lemplate comes with a command line tool call C<lemplate> that you use to
  496. precompile your templates into a Lua module file. For example if you have
  497. a template directory called C<templates> that contains:

  498.     $ ls templates/
  499.     body.tt2
  500.     footer.tt2
  501.     header.tt2

  502. You might run this command:

  503.     $ lemplate --compile template/* > myapp/templates.lua

  504. This will compile all the templates into one Lua module file which can be loaded in your
  505. main OpenResty/Lua application as the module C<myapp.templates>.

  506. Now all you need to do is load the Lua module file in your OpenResty app:

  507.     local templates = require "myapp.templates"

  508. and do the HTML page rendering:

  509.     local results = templates.process("some-page.tt2",
  510.                                       { var1 = val1, var2 = val2, ...})

  511. Now you have Lemplate support for these templates in your OpenResty application.

  512. =head1 Public API

  513. The Lemplate Lua runtime module has the following API method:

  514. =over

  515. =item process(template-name, data)

  516. The C<template-name> is a string like C<'body.tt2'> that is the name of
  517. the top level template that you wish to process.

  518. The optional C<data> specifies the data object to be used by the
  519. templates. It can be an object, a function or a url. If it is an object,
  520. it is used directly. If it is a function, the function is called and the
  521. returned object is used.

  522. =back

  523. =head1 Current Support

  524. The goal of Lemplate is to support all of the Template Toolkit features
  525. that can possibly be supported.

  526. Lemplate now supports almost all the TT directives, including:

  527.     * Plain text
  528.     * [% [GET] variable %]
  529.     * [% CALL variable %]
  530.     * [% [SET] variable = value %]
  531.     * [% DEFAULT variable = value ... %]
  532.     * [% INCLUDE [arguments] %]
  533.     * [% PROCESS [arguments] %]
  534.     * [% BLOCK name %]
  535.     * [% FILTER filter %] text... [% END %]
  536.     * [% WRAPPER template [variable = value ...] %]
  537.     * [% IF condition %]
  538.     * [% ELSIF condition %]
  539.     * [% ELSE %]
  540.     * [% SWITCH variable %]
  541.     * [% CASE [{value|DEFAULT}] %]
  542.     * [% FOR x = y %]
  543.     * [% WHILE expression %]
  544.     * [% RETURN %]
  545.     * [% THROW type message %]
  546.     * [% STOP %]
  547.     * [% NEXT %]
  548.     * [% LAST %]
  549.     * [% CLEAR %]
  550.     * [%# this is a comment %]
  551.     * [% MACRO name(param1, param2) BLOCK %] ... [% END %]

  552. ALL of the string virtual functions are supported.

  553. ALL of the array virtual functions are supported:

  554. ALL of the hash virtual functions are supported:

  555. MANY of the standard filters are implemented.

  556. The remaining features will be added very soon. See the DESIGN document
  557. in the distro for a list of all features and their progress.

  558. =head1 Community

  559. =head2 English Mailing List

  560. The L<openresty-en|https://groups.google.com/group/openresty-en> mailing list is for English speakers.

  561. =head2 Chinese Mailing List

  562. The L<openresty|https://groups.google.com/group/openresty> mailing list is for Chinese speakers.

  563. =head1 Code Repository

  564. The bleeding edge code is available via Git at
  565. git://github.com/openresty/lemplate.git

  566. =head1 Bugs and Patches

  567. Please submit bug reports, wishlists, or patches by

  568. =over

  569. =item 1.

  570. creating a ticket on the L<GitHub Issue Tracker|https://github.com/openresty/lua-nginx-module/issues>,

  571. =item 2.

  572. or posting to the L</Community>.

  573. =back

  574. =head1 CREDIT

  575. This project is based on Ingy dot Net's excellent L<Jemplate> project.

  576. =head1 AUTHOR

  577. Yichun Zhang (agentzh), E<lt>agentzh@gmail.comE<gt>, CloudFlare Inc.

  578. =head1 Copyright

  579. Copyright (C) 2016 Yichun Zhang (agentzh).  All Rights Reserved.

  580. Copyright (C) 1996-2014 Andy Wardley.  All Rights Reserved.

  581. Copyright (c) 2006-2014. Ingy döt Net. All rights reserved.

  582. Copyright (C) 1998-2000 Canon Research Centre Europe Ltd

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

  584. =head1 See Also

  585. =over

  586. =item *

  587. Perl TT2 Reference Manual: http://www.template-toolkit.org/docs/manual/index.html

  588. =item *

  589. Jemplate for compiling TT2 templates to client-side JavaScript: http://www.jemplate.net/

  590. =back