├── minil.toml ├── t ├── templates │ ├── partial │ ├── partial.ext │ ├── partial-with-directives │ ├── inheritance │ ├── partial-not-parsed │ ├── partial-with-recursion │ ├── inheritance-with-directives │ ├── inheritance-with-recursion │ └── test_view.html ├── pod.t ├── pod_coverage.t ├── replace.t ├── variables-coderef.t ├── full.t ├── escape.t ├── regressions.t ├── comments.t ├── file.t ├── inverted-sections.t ├── partials.t ├── template-inheritance.t ├── lambdas.t ├── scope.t ├── variables.t ├── specs.t └── sections.t ├── .gitmodules ├── .gitignore ├── cpanfile ├── Build.PL ├── Changes ├── META.json ├── README.md ├── LICENSE └── lib └── Text └── Caml.pm /minil.toml: -------------------------------------------------------------------------------- 1 | name = "Text-Caml" 2 | -------------------------------------------------------------------------------- /t/templates/partial: -------------------------------------------------------------------------------- 1 | Hello from partial! 2 | -------------------------------------------------------------------------------- /t/templates/partial.ext: -------------------------------------------------------------------------------- 1 | Hello from partial.ext! 2 | -------------------------------------------------------------------------------- /t/templates/partial-with-directives: -------------------------------------------------------------------------------- 1 | Hello {{name}}! 2 | -------------------------------------------------------------------------------- /t/templates/inheritance: -------------------------------------------------------------------------------- 1 | {{$block}}Default Value!{{/block}} 2 | -------------------------------------------------------------------------------- /t/templates/partial-not-parsed: -------------------------------------------------------------------------------- 1 | Hello from partial {{not_parsed}}! 2 | -------------------------------------------------------------------------------- /t/templates/partial-with-recursion: -------------------------------------------------------------------------------- 1 | *{{>partial-with-directives}}* 2 | -------------------------------------------------------------------------------- /t/templates/inheritance-with-directives: -------------------------------------------------------------------------------- 1 | {{$block}}This is a {{value}} value{{/block}} 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ext/spec"] 2 | path = ext/spec 3 | url = https://github.com/mustache/spec 4 | -------------------------------------------------------------------------------- /t/templates/inheritance-with-recursion: -------------------------------------------------------------------------------- 1 | {{ 0; 2 | requires 'File::Spec' => 0; 3 | requires 'Scalar::Util' => 0; 4 | requires 'Test::More' => 0; 5 | -------------------------------------------------------------------------------- /t/templates/test_view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{title}} 4 | 5 | 6 | {{body}} 7 | 8 | 9 | -------------------------------------------------------------------------------- /t/pod.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More; 5 | 6 | eval "use Test::Pod 1.14"; 7 | plan skip_all => "Test::Pod 1.14 required for testing POD" if $@; 8 | 9 | all_pod_files_ok(); 10 | -------------------------------------------------------------------------------- /t/pod_coverage.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More; 5 | 6 | eval "use Test::Pod::Coverage 1.00"; 7 | plan skip_all => "Test::Pod::Coverage 1.00 required for testing POD coverage" 8 | if $@; 9 | 10 | all_pod_coverage_ok(); 11 | -------------------------------------------------------------------------------- /t/replace.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More tests => 1; 5 | 6 | use Text::Caml; 7 | 8 | my $renderer = Text::Caml->new; 9 | 10 | my $output = $renderer->render('{{-hello}}'); 11 | is $output => '{{hello}}', 'can escape with {{-hello}} for literal {{hello}}'; 12 | -------------------------------------------------------------------------------- /t/variables-coderef.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More tests => 1; 5 | 6 | use Text::Caml; 7 | 8 | my $renderer = Text::Caml->new; 9 | 10 | my $output = $renderer->render( 11 | '{{foo}}', 12 | { foo => sub {'bar'} 13 | } 14 | ); 15 | is $output => 'bar', 'sub returning "bar" renders as "bar"'; 16 | -------------------------------------------------------------------------------- /Build.PL: -------------------------------------------------------------------------------- 1 | # ========================================================================= 2 | # THIS FILE IS AUTOMATICALLY GENERATED BY MINILLA. 3 | # DO NOT EDIT DIRECTLY. 4 | # ========================================================================= 5 | 6 | use 5.008_001; 7 | use strict; 8 | 9 | use Module::Build::Tiny 0.035; 10 | 11 | Build_PL(); 12 | 13 | -------------------------------------------------------------------------------- /t/full.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More tests => 1; 5 | 6 | use Text::Caml; 7 | 8 | my $renderer = Text::Caml->new; 9 | 10 | my $output = $renderer->render( 11 | <<'EOF', {name => 'Chris', value => 10000, taxed_value => 10000 - (10000 * 0.4), in_ca => 1}); 12 | Hello {{name}} 13 | You have just won ${{value}}! 14 | {{#in_ca}} 15 | Well, ${{taxed_value}}, after taxes. 16 | {{/in_ca}} 17 | EOF 18 | 19 | my $expected = <<'EOF'; 20 | Hello Chris 21 | You have just won $10000! 22 | Well, $6000, after taxes. 23 | EOF 24 | chomp $expected; 25 | 26 | is $output => $expected, 'full test checking many features'; 27 | -------------------------------------------------------------------------------- /t/escape.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More tests => 4; 5 | 6 | use Text::Caml; 7 | 8 | my $renderer = Text::Caml->new; 9 | 10 | my $output = $renderer->render('{{foo}}', {foo => '&bar;'}); 11 | is $output => '<b>&bar;</b>', 'escape HTML by default'; 12 | 13 | $output = $renderer->render('{{&foo}}', {foo => '&bar;'}); 14 | is $output => '&bar;', '& disable escaping'; 15 | 16 | $renderer = Text::Caml->new(do_not_escape => 1); 17 | 18 | $output = $renderer->render('{{foo}}', {foo => '&bar;'}); 19 | is $output => '&bar;', 'do_not_escape mode'; 20 | 21 | $output = $renderer->render('{{&foo}}', {foo => '&bar;'}); 22 | is $output => '<b>&bar;</b>', '& switches between modes'; 23 | -------------------------------------------------------------------------------- /t/regressions.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More tests => 1; 5 | 6 | use Text::Caml; 7 | 8 | use File::Basename (); 9 | use File::Spec (); 10 | 11 | # Had '@_' instead of 'ref $_[0] eq 'HASH' ? $_[0] : {@_}' in render_file. 12 | # Without this test no tests actually caught that bug. 13 | { 14 | my $renderer = Text::Caml->new; 15 | 16 | my $templates_path = File::Spec->catfile( 17 | File::Basename::dirname(__FILE__), 'templates' 18 | ); 19 | 20 | my $output = $renderer->render_file( 21 | File::Spec->catfile($templates_path, 'partial-with-directives'), 22 | name => "Alex", 23 | ); 24 | 25 | is( 26 | $output, 27 | 'Hello Alex!', 28 | 'handle non-hashref context in render_file()' 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /t/comments.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More tests => 5; 5 | 6 | use Text::Caml; 7 | 8 | my $renderer = Text::Caml->new; 9 | 10 | my $output = 11 | $renderer->render('foo{{! Comment}}bar', {'! Comment' => 'ooops'}); 12 | is $output => 'foobar', 'in-line {{! Comment}}'; 13 | 14 | $output = 15 | $renderer->render("foo{{!\n Comment\n}}bar", {"!\n Comment\n" => 'ooops'}); 16 | is $output => 'foobar', 'multi-line {{! Comment}}'; 17 | 18 | $output = 19 | $renderer->render("foo\n{{! Comment}}\nbar", {"! Comment" => 'ooops'}); 20 | is $output => "foo\nbar", 'trailing new-line {{! Comment}}'; 21 | 22 | $output = 23 | $renderer->render("foo\n {{! Comment}}\nbar", {"! Comment" => 'ooops'}); 24 | is $output => "foo\nbar", 'a comment is not a variable name (with new-line)'; 25 | 26 | $output = 27 | $renderer->render("foo {{! Comment}} bar", {"! Comment" => 'ooops'}); 28 | is $output => "foo bar", 'a comment is not a variable name (no new-line)'; 29 | -------------------------------------------------------------------------------- /t/file.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More tests => 4; 5 | 6 | use Text::Caml; 7 | 8 | use Cwd (); 9 | use File::Basename (); 10 | use File::Spec (); 11 | 12 | my $renderer = Text::Caml->new; 13 | my $templates_path = 14 | File::Spec->catfile(File::Basename::dirname(__FILE__), 'templates'); 15 | 16 | my $output = 17 | $renderer->render_file(File::Spec->catfile($templates_path, 'partial')); 18 | is $output, 'Hello from partial!', "partial rendered from relative path ($templates_path)"; 19 | 20 | $renderer = Text::Caml->new; 21 | $templates_path = Cwd::abs_path($templates_path); 22 | 23 | $output = 24 | $renderer->render_file(File::Spec->catfile($templates_path, 'partial')); 25 | is $output, 'Hello from partial!', "partial rendered from full path ($templates_path)"; 26 | 27 | $renderer = Text::Caml->new(templates_path => $templates_path); 28 | 29 | $output = $renderer->render_file('partial'); 30 | is $output, 'Hello from partial!', "partial rendered from renderer path ($templates_path)"; 31 | 32 | eval { $output = $renderer->render_file('no_such_file') }; 33 | ok $@, 'exception thrown on non-existent partial file'; 34 | -------------------------------------------------------------------------------- /t/inverted-sections.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More tests => 6; 5 | 6 | use Text::Caml; 7 | 8 | my $renderer = Text::Caml->new; 9 | 10 | my $template = <<'EOF'; 11 | {{#repo}} 12 | {{name}} 13 | {{/repo}} 14 | {{^repo}} 15 | No repos :( 16 | {{/repo}} 17 | EOF 18 | 19 | is $renderer->render($template, {repo => []}) => ' No repos :(', 'inverted section {{^repo}} works for emtpy arrayref'; 20 | 21 | is $renderer->render($template, {repo => [{name => 'repo'}]}) => 22 | ' repo', 'inverted section {{^repo}} not matched for non-empty arrayref'; 23 | 24 | $template = <<'EOF'; 25 | {{text}} 26 | {{^text}} 27 | No text 28 | {{/text}} 29 | EOF 30 | 31 | is $renderer->render($template, {text => 'exists'}) => "exists\n", 'inverted section {{^text}} not matched for non-empty string'; 32 | is $renderer->render($template, {text => ''}) => ' No text', 'inverted section {{^text}} matched for empty string'; 33 | 34 | $template = <<'EOF'; 35 | {{text.body}} 36 | {{^text.body}} 37 | Text not exists 38 | {{/text.body}} 39 | EOF 40 | 41 | is $renderer->render($template, {text => {body => 'text exists'}}) => "text exists\n", 'inverted section {{^text.body}} not matched for non-empty hashref'; 42 | is $renderer->render($template, {text => {body => ''}}), ' Text not exists', 'inverted section {{^text.body}} matched for empty string in non-empty hashref'; 43 | -------------------------------------------------------------------------------- /t/partials.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More; 5 | 6 | use Text::Caml; 7 | 8 | my $renderer = Text::Caml->new(templates_path => 't/templates'); 9 | 10 | my $output = $renderer->render('{{>partial}}'); 11 | is $output => 'Hello from partial!', 'partial renders correctly {{>partial}}'; 12 | 13 | $output = $renderer->render('{{>partial-with-directives}}', {name => 'foo'}); 14 | is $output => 'Hello foo!', 'partial can use directives {{>partial-with-directives}}'; 15 | 16 | $output = $renderer->render('{{>partial-with-recursion}}', {name => 'foo'}); 17 | is $output => '*Hello foo!*', 'partials can recurse {{>partial-with-recursion}}'; 18 | 19 | $output = $renderer->render('{{> partial }}'); 20 | is $output => 'Hello from partial!', 'partial call ignores spaces, eg. {{^ partial }}'; 21 | 22 | subtest 'render partial with default extension' => sub { 23 | my $renderer = Text::Caml->new( 24 | templates_path => 't/templates', 25 | default_partial_extension => 'ext' 26 | ); 27 | 28 | my $output = $renderer->render('{{> partial}}'); 29 | is $output => 'Hello from partial.ext!'; 30 | }; 31 | 32 | subtest 'render partial without parsing' => sub { 33 | my $renderer = Text::Caml->new(templates_path => 't/templates'); 34 | 35 | my $output = $renderer->render('{{>&partial-not-parsed}}'); 36 | is $output => 'Hello from partial {{not_parsed}}!'; 37 | }; 38 | 39 | done_testing; 40 | -------------------------------------------------------------------------------- /t/template-inheritance.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More; 5 | 6 | use Text::Caml; 7 | 8 | my $renderer = Text::Caml->new(templates_path => 't/templates'); 9 | 10 | # testing scenario where we provide the replacement content for a block 11 | my $output = $renderer->render('{{ 'Replaced Value!', 'basic inheritance works correctly'; 13 | 14 | # testing the scenario where no replacement is provided for a block and must use the default value 15 | $output = $renderer->render('{{ 'Default Value!', 'basic inheritance uses default values if no custom one provided for blocks'; 17 | 18 | # testing directive use in replacement block 19 | $output = $renderer->render('{{ 'boss'}); 20 | is $output => 'Hello boss!', 'directives work ok in replacement blocks'; 21 | 22 | # testing directive use in default block values 23 | $output = $renderer->render('{{ 'default'}); 24 | is $output => 'This is a default value', 'directives work ok in default block values'; 25 | 26 | # testing a more complex scenario with multiple levels of inheritance 27 | $output = $renderer->render('{{ 'I\'m recursive', 'recursive inheritance works ok'; 29 | 30 | done_testing; 31 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | Revision history for perl module Text::Caml 2 | 3 | {{$NEXT}} 4 | 5 | 0.17 2018-08-31T20:16:26Z 6 | 7 | - Non-escape mode (Igor Lobanov) 8 | 9 | 0.16 2017-05-03T10:42:27Z 10 | 11 | - Escape "{" and "}" (Shoichi Kaji) 12 | 13 | 0.15 2017-04-21T23:19:09Z 14 | 15 | - Not parsed partials 16 | 17 | 0.14 2016-04-18T22:07:12Z 18 | 19 | - Implement array indexing (Fernando Oliveira) 20 | 21 | 0.13 2015-09-03T06:55:41Z 22 | 23 | - Fixed the way we parse the blocks to ensure we provide correct error messages in case there is a syntax error and no 24 | closing block tag is present (Ovidiu Stateina) 25 | - Add more relevant error messages, improve block parsing. Work in progress (Ovidiu Stateina) 26 | - Added support for nested templates (Ovidiu Stateina) 27 | 28 | 0.12 2015-06-22T17:20:50Z 29 | 30 | - Added default_partial_extension option 31 | 32 | 0.11 2015-06-22T17:11:54Z 33 | 34 | - Added template generation possibility 35 | - Fix a hashref "pollution" issue (Andrew Rodland) 36 | - Add regression test to check that $context arg handling is correct (Alex Balhatchet) 37 | - Remove unnecessary check on $context in _parse() (Alex Balhatchet) 38 | - Fix typo in render_file() use of @_ (Alex Balhatchet) 39 | 40 | 0.10 2012-03-29 41 | 42 | - Fixed hash contexts merging 43 | 44 | 0.00905 2011-10-04 45 | 46 | - Fixed 'with' behaviour 47 | - Implemented smth like Pascal's with 48 | - Context checks for available method too 49 | 50 | 0.00904 2011-09-25 51 | 52 | - Allow objects passing 53 | - Render template before passing to lambda 54 | - Fixed inverted section bug (Sergey Zasenko) 55 | 56 | 0.00903 2011-08-22 57 | 58 | - Fixed absolute template path rendering 59 | -------------------------------------------------------------------------------- /t/lambdas.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More tests => 7; 5 | 6 | use Text::Caml; 7 | 8 | my $renderer = Text::Caml->new; 9 | my $output; 10 | 11 | $output = $renderer->render( 12 | '{{lamda}}', 13 | { lamda => sub { } 14 | } 15 | ); 16 | is $output => '', 'empty sub works'; 17 | 18 | $output = $renderer->render( 19 | '{{lamda}}', 20 | { lamda => sub {0} 21 | } 22 | ); 23 | is $output => '0', 'sub returning "0" renders as "0"'; 24 | 25 | $output = $renderer->render( 26 | '{{lamda}}', 27 | { lamda => sub {'text'} 28 | } 29 | ); 30 | is $output => 'text', 'sub returning "text" renders as "text"'; 31 | 32 | $output = $renderer->render( 33 | '{{lamda}}', 34 | { lamda => sub {'{{var}}'}, 35 | var => 'text' 36 | } 37 | ); 38 | is $output => 'text', 'sub returning partial "{{var}}" renders as expected'; 39 | 40 | $output = $renderer->render( 41 | '{{#lamda}}Hello{{/lamda}}', 42 | { lamda => sub {'{{var}}'}, 43 | var => 'text' 44 | } 45 | ); 46 | is $output => 'text', 'sub can be used as {{#lambda}}'; 47 | 48 | my $wrapped = sub { 49 | my $self = shift; 50 | my $text = shift; 51 | 52 | return '' . $self->render($text, @_) . ''; 53 | }; 54 | 55 | $output = $renderer->render(<<'EOF', {name => 'Willy', wrapped => $wrapped}); 56 | {{#wrapped}} 57 | {{name}} is awesome. 58 | {{/wrapped}} 59 | EOF 60 | is $output => "Willy is awesome.", 'sub takes renderer as first parameter and template text as second parameter'; 61 | 62 | $output = $renderer->render(<<'EOF', {wrapper => sub {$_[1] =~ s/r/z/; $_[1]}, list => [qw/foo bar/]}); 63 | {{#list}} 64 | {{#wrapper}} 65 | {{.}} 66 | {{/wrapper}} 67 | {{/list}} 68 | EOF 69 | like $output => qr/foo\s+baz/, 'sub can manipulate the template text directly'; 70 | -------------------------------------------------------------------------------- /t/scope.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use Test::More tests => 8; 5 | 6 | use Text::Caml; 7 | 8 | my $renderer = Text::Caml->new(templates_path => 't/templates'); 9 | 10 | my $output = $renderer->render('{{hello}}', {hello => 'bar'}); 11 | is $output => 'bar', 'base scope works as expected'; 12 | 13 | $output = $renderer->render( 14 | '{{#section}}{{hello}}{{/section}}', 15 | { section => 'ok', 16 | hello => 'bar' 17 | } 18 | ); 19 | is $output => 'bar', 'section scope falls back on base scope if section is scalar'; 20 | 21 | $output = $renderer->render( 22 | '{{#section}}{{hello}}{{/section}}', 23 | { section => [{foo => 'bar'}], 24 | hello => 'bar' 25 | } 26 | ); 27 | is $output => 'bar', 'section scope falls back on base scope if hello does not exist in section arrayref'; 28 | 29 | $output = $renderer->render('{{#section}}{{hello}}{{/section}}', 30 | {section => {hello => 'bar'}, hello => 'foo'}); 31 | is $output => 'bar', 'section scope overrides base scope if hello exists in both'; 32 | 33 | $output = $renderer->render('{{#section}}{{hello.bar}}{{/section}}', 34 | {section => {}, hello => {bar => 'foo'}}); 35 | is $output => 'foo', 'base scope works for more complex declaration {{hello.bar}}'; 36 | 37 | $output = $renderer->render('{{name}} {{>partial-with-directives}}', {name => 'foo'}); 38 | is $output => 'foo Hello foo!', 'base scope works outside and inside partials'; 39 | 40 | my @array = ( 41 | { foo => 'a' }, 42 | { foo => 'b' }, 43 | ); 44 | 45 | $output = $renderer->render( 46 | '{{#array}}{{foo}}{{_idx}}{{/array}}', 47 | { 48 | array => \@array 49 | } 50 | ); 51 | is $output => 'a0b1', 'special variable {{_idx}} gives index for array loop'; 52 | ok !defined $array[0]{_idx}, '$array[0]{_idx} not inadvertantly created on the hashref'; 53 | -------------------------------------------------------------------------------- /META.json: -------------------------------------------------------------------------------- 1 | { 2 | "abstract" : "Mustache template engine", 3 | "author" : [ 4 | "Viacheslav Tykhanovskyi, C" 5 | ], 6 | "dynamic_config" : 0, 7 | "generated_by" : "Minilla/v3.0.13, CPAN::Meta::Converter version 2.150010", 8 | "license" : [ 9 | "artistic_2" 10 | ], 11 | "meta-spec" : { 12 | "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", 13 | "version" : "2" 14 | }, 15 | "name" : "Text-Caml", 16 | "no_index" : { 17 | "directory" : [ 18 | "t", 19 | "xt", 20 | "inc", 21 | "share", 22 | "eg", 23 | "examples", 24 | "author", 25 | "builder" 26 | ] 27 | }, 28 | "prereqs" : { 29 | "configure" : { 30 | "requires" : { 31 | "Module::Build::Tiny" : "0.035" 32 | } 33 | }, 34 | "develop" : { 35 | "requires" : { 36 | "Test::CPAN::Meta" : "0", 37 | "Test::MinimumVersion::Fast" : "0.04", 38 | "Test::PAUSE::Permissions" : "0.04", 39 | "Test::Pod" : "1.41", 40 | "Test::Spellunker" : "v0.2.7" 41 | } 42 | }, 43 | "runtime" : { 44 | "requires" : { 45 | "Carp" : "0", 46 | "File::Spec" : "0", 47 | "Scalar::Util" : "0", 48 | "Test::More" : "0" 49 | } 50 | } 51 | }, 52 | "release_status" : "unstable", 53 | "resources" : { 54 | "bugtracker" : { 55 | "web" : "https://github.com/vti/text-caml/issues" 56 | }, 57 | "homepage" : "https://github.com/vti/text-caml", 58 | "repository" : { 59 | "type" : "git", 60 | "url" : "git://github.com/vti/text-caml.git", 61 | "web" : "https://github.com/vti/text-caml" 62 | } 63 | }, 64 | "version" : "0.17", 65 | "x_contributors" : [ 66 | "Alex Balhatchet ", 67 | "Andrew Rodland ", 68 | "Fernando Oliveira ", 69 | "Igor Lobanov ", 70 | "Michael Ivanchenko ", 71 | "Ovidiu Stateina ", 72 | "Sergey Zasenko ", 73 | "Shoichi Kaji ", 74 | "Viacheslav Tykhanovskyi ", 75 | "Yves Chevallier ", 76 | "vti " 77 | ], 78 | "x_serialization_backend" : "JSON::PP version 2.27400_02" 79 | } 80 | -------------------------------------------------------------------------------- /t/variables.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | package Foo; 5 | 6 | sub new { 7 | shift; 8 | bless {@_}; 9 | } 10 | sub method { shift->{bar} } 11 | 12 | package main; 13 | 14 | use Test::More tests => 18; 15 | 16 | use Text::Caml; 17 | 18 | my $renderer = Text::Caml->new; 19 | 20 | my $output = $renderer->render(''); 21 | is $output => '', 'empty string renders as empty'; 22 | 23 | $output = $renderer->render('foo'); 24 | is $output => 'foo', 'bare "foo" string renders as "foo"'; 25 | 26 | $output = $renderer->render('Hello, [user]!'); 27 | is $output => 'Hello, [user]!', 'cannot access variable with square brackets [user]'; 28 | 29 | $output = $renderer->render('Hello, {{user}}!'); 30 | is $output => 'Hello, !', 'non-existent variable lookup renders as empty string {{user}}'; 31 | 32 | $output = $renderer->render('Hello, {{user}}!', {user => 'vti'}); 33 | is $output => 'Hello, vti!', 'variable lookup to string renders as string {{user}}'; 34 | 35 | $output = $renderer->render('{{var}}', {var => 1}); 36 | is $output => '1', 'variable lookup to number renders as number {{var}}'; 37 | 38 | $output = $renderer->render('{{var}}', {var => 0}); 39 | is $output => '0', 'variable lookup to zero (0) renders as number {{var}}'; 40 | 41 | $output = $renderer->render('{{var}}', {var => ''}); 42 | is $output => '', 'variable lookup to empty string renders as empty string {{var}}'; 43 | 44 | $output = $renderer->render('{{var}}', {var => undef}); 45 | is $output => '', 'variable lookup as undef renders as empty string {{var}}'; 46 | 47 | $output = $renderer->render('{{var}}', {var => '1 > 2'}); 48 | is $output => '1 > 2', 'HTML escaping of ">" renders as ">"'; 49 | 50 | $output = $renderer->render('{{&var}}', {var => '1 > 2'}); 51 | is $output => '1 > 2', '{{&var}} turns off HTML escaping'; 52 | 53 | $output = $renderer->render('{{{var}}}', {var => '1 > 2'}); 54 | is $output => '1 > 2', '{{{var}}} turns off HTML escaping'; 55 | 56 | $output = $renderer->render('{{foo.bar}}', {foo => {bar => 'baz'}}); 57 | is $output => 'baz', '{{foo.bar}} variable lookup descends into hashref'; 58 | 59 | $output = $renderer->render('{{foo.bak}}', {foo => {bar => 'baz'}}); 60 | is $output => '', '{{foo.bak}} non-existent key renders as empty string'; 61 | 62 | $output = $renderer->render('{{f1o.bak}}'); 63 | is $output => '', '{{foo.bak}} non-existent hashref foo renders as empty string'; 64 | 65 | $output = $renderer->render('{{foo.method}}', {foo => Foo->new(bar => 'baz')}); 66 | is $output => 'baz', '{{foo.method}} object method call renders as return value of method'; 67 | 68 | $output = $renderer->render('{{foo.0}}', {foo => [qw/bar baz/]}); 69 | is $output => 'bar', 'get index of a array'; 70 | 71 | $output = $renderer->render('{{{foo.1.text}}}', {foo => [{text => "bar"}, {text => "baz"}]}); 72 | is $output => 'baz', 'get a has as a index of a array'; 73 | -------------------------------------------------------------------------------- /t/specs.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | use File::Path qw(make_path remove_tree); 5 | use File::Basename qw(basename); 6 | use FindBin '$Bin'; 7 | use Text::Caml; 8 | 9 | # To run the tests, set the environment variable like this: 10 | # RUN_SPECS=1 prove t/specs.t 11 | 12 | BEGIN { 13 | require Test::More; 14 | 15 | if ( $ENV{RUN_SPECS} ) { 16 | require YAML::XS; 17 | import Test::More tests => 124; 18 | } 19 | else { 20 | import Test::More skip_all 21 | => 'to run the tests set the environment variable:' 22 | . ' RUN_SPECS=1 prove ' . __FILE__ ; 23 | } 24 | } 25 | 26 | my $PARTS_DIR = "$Bin/partials/"; 27 | my $engine = Text::Caml->new; 28 | $engine->set_templates_path( $PARTS_DIR ); 29 | 30 | sub startup { 31 | # remove partials directory 32 | &shutdown(); 33 | 34 | # create partials directory 35 | make_path( $PARTS_DIR, { error => \my $err } ); 36 | if ( @$err ) { 37 | for my $diag ( @$err ) { 38 | my ($file, $message) = %$diag; 39 | 40 | if ( $file eq '' ) { 41 | die "General error: [$message]"; 42 | } 43 | else { 44 | die "Can't create [$file]: [$message]"; 45 | } 46 | } 47 | } 48 | } 49 | 50 | sub setup { 51 | my $t = shift; 52 | 53 | # create and fill partials files 54 | foreach my $k ( keys %{ $t->{partials} } ) { 55 | my $parts_filename = $PARTS_DIR . $k; 56 | 57 | open my $fh, '>', $parts_filename 58 | or die "Can't create [$parts_filename]: [$!]"; 59 | print $fh $t->{partials}->{$k}; 60 | close $fh; 61 | } 62 | } 63 | 64 | sub teardown { 65 | my $t = shift; 66 | 67 | # remove partials files 68 | foreach my $k ( keys %{ $t->{partials} } ) { 69 | my $parts_filename = $PARTS_DIR . $k; 70 | 71 | unless ( unlink $parts_filename ) { 72 | die "Can't remove [$parts_filename]: [$!]"; 73 | } 74 | } 75 | } 76 | 77 | sub shutdown { 78 | # remove partials directory 79 | remove_tree( $PARTS_DIR, { error => \my $err } ); 80 | if ( @$err ) { 81 | for my $diag ( @$err ) { 82 | my ($file, $message) = %$diag; 83 | 84 | if ( $file eq '' ) { 85 | die "General error: [$message]"; 86 | } 87 | else { 88 | die "Can't remove [$file]: [$message]"; 89 | } 90 | } 91 | } 92 | } 93 | 94 | while ( my $filename = <$Bin/../ext/spec/specs/*.yml> ) { 95 | my $basename = basename $filename; 96 | 97 | startup(); 98 | 99 | my $spec = YAML::XS::LoadFile($filename); 100 | my $tests = $spec->{tests}; 101 | 102 | note "\n---------\n$spec->{overview}"; 103 | 104 | foreach my $t ( @{$tests} ) { 105 | setup($t); 106 | 107 | $t->{signature} = "$basename: $t->{name}\n$t->{desc}\n"; 108 | my $out = ''; 109 | 110 | eval { 111 | # ensure that lambdas are properly setup 112 | my $data = $t->{data}; 113 | my @hashes = $data; 114 | for my $hash ( @hashes ) { 115 | while ( my ($k, $v) = each %$hash ) { 116 | $hash->{$k} = eval $v->{perl} if ref $v eq 'code'; 117 | push @hashes, $v if ref $v eq 'HASH'; 118 | } 119 | } 120 | $out = $engine->render( $t->{template}, $t->{data} ); 121 | }; 122 | if ( $@ ) { 123 | fail( $t->{signature} . "ERROR: $@" ); 124 | } 125 | else { 126 | is $out => $t->{expected}, $t->{signature}; 127 | } 128 | 129 | teardown($t); 130 | } 131 | 132 | &shutdown(); 133 | } 134 | -------------------------------------------------------------------------------- /t/sections.t: -------------------------------------------------------------------------------- 1 | use strict; 2 | use warnings; 3 | 4 | package Foo; 5 | 6 | sub new { 7 | shift; 8 | bless {@_}; 9 | } 10 | 11 | sub method { shift->{values} } 12 | 13 | package main; 14 | 15 | use Test::More; 16 | 17 | use Text::Caml; 18 | 19 | my $renderer = Text::Caml->new; 20 | my $output; 21 | 22 | $output = $renderer->render('{{#bool}}Hello{{/bool}}', {bool => 1}); 23 | is $output => 'Hello', 'boolean section true enters section'; 24 | 25 | $output = $renderer->render('{{#bool}}Hello{{/bool}}', {bool => Foo->new}); 26 | is $output => 'Hello', 'boolean section object enters section'; 27 | 28 | $output = $renderer->render('{{#bool.method}}Hello{{/bool.method}}', 29 | {bool => Foo->new}); 30 | is $output => '', 'boolean section object method call which returns undef does not enter section'; 31 | 32 | $output = $renderer->render( 33 | '{{#bool.method}}Hello{{/bool.method}}', 34 | {bool => Foo->new(values => 1)} 35 | ); 36 | is $output => 'Hello', 'boolean section object method call which returns 1 enters section'; 37 | 38 | $output = $renderer->render('{{#bool}}{{method}}{{/bool}}', 39 | {bool => Foo->new(values => '1')}); 40 | is $output => '1', 'boolean section object method call (with scope) which returns 1 enters section'; 41 | 42 | $output = $renderer->render('{{#bool}}{{#method}}{{method}}{{/method}}{{/bool}}', 43 | {bool => Foo->new(values => '1')}); 44 | is $output => '1', 'boolean section object method call with existence check {{#method}} which returns 1 enters section'; 45 | 46 | $output = $renderer->render('{{#bool}}Hello{{/bool}}', {bool => 0}); 47 | is $output => '', 'boolean section false does not enter section'; 48 | 49 | $output = $renderer->render("{{#bool}}\nHello\n{{/bool}}", {bool => 0}); 50 | is $output => '', 'boolean section false (with new-lines) does not enter section'; 51 | 52 | $output = 53 | $renderer->render("{{#bool}}\nHello\n{{/bool}}\n{{unknown}}", {bool => 0}); 54 | is $output => '', 'boolean section false (with even more new-lines) does not enter section'; 55 | 56 | $output = 57 | $renderer->render('{{#list}}{{n}}{{/list}}', 58 | {list => [{n => 1}, {n => 2}, {n => 3}]}); 59 | is $output => '123', 'list section loops over list with scope'; 60 | 61 | $output = $renderer->render('{{#list}}{{.}}{{/list}}', {list => [1, 2, 3]}); 62 | is $output => '123', 'list section loops over list and {{.}} gives current list item'; 63 | 64 | $output = $renderer->render('{{#foo.list}}{{.}}{{/foo.list}}', 65 | {foo => {list => [1, 2, 3]}}); 66 | is $output => '123', 'list section {{#foo.list}} loops over list and {{.}} gives current list item'; 67 | 68 | $output = $renderer->render('{{#list}}{{n}}{{/list}}', {list => []}); 69 | is $output => '', 'empty list has no values'; 70 | 71 | $output = 72 | $renderer->render('{{#list}}{{_idx}}{{/list}}', {list => [1, 2, 3]}); 73 | is $output => '012', 'special variable {{_idx}} gives list item index (zero-based)'; 74 | 75 | $output = $renderer->render('{{#list}}{{#_even}}{{.}}{{/_even}}{{/list}}', 76 | {list => [1, 2, 3]}); 77 | is $output => '13', 'special check {{_even}} works (based on index)'; 78 | 79 | $output = $renderer->render('{{#list}}{{#_odd}}{{.}}{{/_odd}}{{/list}}', 80 | {list => [1, 2, 3]}); 81 | is $output => '2', 'special check {{_odd}} works (based on index)'; 82 | 83 | $output = $renderer->render('{{#list}}{{^_first}}, {{/_first}}{{.}}{{/list}}', 84 | {list => [1, 2, 3]}); 85 | is $output => '1, 2, 3', 'special check {{_first}} checks for index zero'; 86 | 87 | $output = $renderer->render('{{#list}}{{.}}{{^_last}}, {{/_last}}{{/list}}', 88 | {list => [1, 2, 3]}); 89 | is $output => '1, 2, 3', 'special check {{_last}} checks for last index'; 90 | 91 | $output = $renderer->render('{{#list}}{{#.}}{{.}}{{/.}}{{/list}}', 92 | {list => [[1], [2], [3]]}); 93 | is $output => '123', 'loop over lists of lists with {{#.}}'; 94 | 95 | $output = $renderer->render('{{#list}}{{method}}{{/list}}', 96 | {list => [Foo->new(values => 1), Foo->new(values => 2)]}); 97 | is $output => '12', 'list of objects with method calls'; 98 | 99 | $output = 100 | $renderer->render('{{#s}}one{{/s}} {{#s}}{{two}}{{/s}} {{#s}}three{{/s}}', 101 | {s => 1, two => 'TWO'}); 102 | is $output => 'one TWO three', 'variable lookup within boolean check scope works'; 103 | 104 | $output = $renderer->render('{{# bool }}Hello{{/bool}}', {bool => 1}); 105 | is $output => 'Hello', 'ignore scpaces {{# bool }}'; 106 | 107 | done_testing; 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | Text::Caml - Mustache template engine 4 | 5 | # SYNOPSIS 6 | 7 | my $view = Text::Caml->new; 8 | 9 | my $output = $view->render_file('template', {title => 'Hello', body => 'there!'}); 10 | 11 | # template 12 | 13 | 14 | {{title}} 15 | 16 | 17 | {{body}} 18 | 19 | 20 | 21 | $output = $view->render('{{hello}}', {hello => 'hi'}); 22 | 23 | # DESCRIPTION 24 | 25 | [Text::Caml](https://metacpan.org/pod/Text::Caml) is a Mustache-like ([http://mustache.github.com/](http://mustache.github.com/)) template engine. 26 | That means it tends to have no logic in template files. 27 | 28 | ## Syntax 29 | 30 | ### Context 31 | 32 | Context is the data passed to the template. Context can change during template 33 | rendering and be specific in various cases. 34 | 35 | ### Variables 36 | 37 | Variables are inserted using `{{foo}}` syntax. If a variable is not defined or 38 | empty it is simply ignored. 39 | 40 | Hello {{user}}! 41 | 42 | By default every variable is escaped when parsed. This can be omitted using `&` 43 | flag. 44 | 45 | # user is '1 > 2' 46 | Hello {{user}}! => Hello 1 > 2! 47 | 48 | Hello {{&user}}! => Hello 1 > 2! 49 | 50 | Using a `.` syntax it is possible to access deep hash structures. 51 | 52 | # user => {name => 'Larry'} 53 | {{user.name}} 54 | 55 | Larry 56 | 57 | ### Comments 58 | 59 | Comments are ignored. They can be multiline too. 60 | 61 | foo{{! Comment}}bar 62 | 63 | foo{{! 64 | Comment 65 | }}bar 66 | 67 | ### Sections 68 | 69 | Sections are like iterators that iterate over your data. Depending on a 70 | variable type different iterators are created. 71 | 72 | - Boolean, `have_comments` is defined, not zero and not empty. 73 | 74 | # have_comments => 1 75 | {{#have_comments}} 76 | We have comments! 77 | {{/have_comments}} 78 | 79 | We have comments! 80 | 81 | - Array, `list` is a non-empty array reference. Special variable `{{.}}` is 82 | created to point to the current element. 83 | 84 | # list => [1, 2, 3] 85 | {{#list}}{{.}}{{/list}} 86 | 87 | 123 88 | 89 | - Hash, `hash` is a non-empty hash reference. Context is swithed to the 90 | elements. 91 | 92 | # hash => {one => 1, two => 2, three => 3} 93 | {{#hash}} 94 | {{one}}{{two}}{{three}} 95 | {{/hash}} 96 | 97 | 123 98 | 99 | - Lambda, `lambda` is an anonymous subroutine, that's called with three 100 | arguments: current object instance, template and the context. This can be used 101 | for subrendering, helpers etc. 102 | 103 | wrapped => sub { 104 | my $self = shift; 105 | my $text = shift; 106 | 107 | return '' . $self->render($text, @_) . ''; 108 | }; 109 | 110 | {{#wrapped}} 111 | {{name}} is awesome. 112 | {{/wrapped}} 113 | 114 | Willy is awesome. 115 | 116 | ### Inverted sections 117 | 118 | Inverted sections are run in those situations when normal sections don't. When 119 | boolean value is false, array is empty etc. 120 | 121 | # repo => [] 122 | {{#repo}} 123 | {{name}} 124 | {{/repo}} 125 | {{^repo}} 126 | No repos :( 127 | {{/repo}} 128 | 129 | No repos :( 130 | 131 | ### Partials 132 | 133 | Partials are like `inludes` in other templates engines. They are run with the 134 | current context and can be recursive. 135 | 136 | {{#articles}} 137 | {{>article_summary}} 138 | {{/articles}} 139 | 140 | If you want to include another file without parsing, prefix the filename with `&`: 141 | 142 | {{#articles}} 143 | {{>&article_summary_not_parsed}} 144 | {{/articles}} 145 | 146 | ### Nested Templates 147 | 148 | This gives horgan.js style template inheritance. 149 | 150 | {{! header.mustache }} 151 | 152 | {{$title}}Default title{{/title}} 153 | 154 | 155 | {{! base.mustache }} 156 | 157 | {{$header}}{{/header}} 158 | {{$content}}{{/content}} 159 | 160 | 161 | {{! mypage.mustache }} 162 | {{Hello world 171 | {{/content}} 172 | {{/base}} 173 | 174 | Rendering mypage.mustache would output: 175 | My page title

Hello world

176 | 177 | # ATTRIBUTES 178 | 179 | ## `templates_path` 180 | 181 | my $path = $engine->templates_path; 182 | 183 | Return path where templates are searched. 184 | 185 | ## `set_templates_path` 186 | 187 | my $path = $engine->set_templates_path('templates'); 188 | 189 | Set base path under which templates are searched. 190 | 191 | ## `default_partial_extension` 192 | 193 | If this option is set that the extension is automatically added to the partial 194 | filenames. 195 | 196 | my $engine = Text::Caml->new(default_partial_extension => 'caml'); 197 | 198 | --- 199 | {{#articles}} 200 | {{>article_summary}} # article_summary.caml will be searched 201 | {{/articles}} 202 | 203 | # METHODS 204 | 205 | ## `new` 206 | 207 | my $engine = Text::Caml->new; 208 | 209 | Create a new [Text::Caml](https://metacpan.org/pod/Text::Caml) object. 210 | 211 | ## `render` 212 | 213 | $engine->render('{{foo}}', {foo => 'bar'}); 214 | 215 | Render template from string. 216 | 217 | ## `render_file` 218 | 219 | $engine->render_file('template.mustache', {foo => 'bar'}); 220 | 221 | Render template from file. 222 | 223 | # DEVELOPMENT 224 | 225 | ## Repository 226 | 227 | http://github.com/vti/text-caml 228 | 229 | # AUTHOR 230 | 231 | Viacheslav Tykhanovskyi, `vti@cpan.org` 232 | 233 | # CREDITS 234 | 235 | Sergey Zasenko (und3f) 236 | 237 | Andrew Rodland (arodland) 238 | 239 | Alex Balhatchet (kaoru) 240 | 241 | Yves Chevallier 242 | 243 | Ovidiu Stateina 244 | 245 | Fernando Oliveira 246 | 247 | Shoichi Kaji (skaji) 248 | 249 | # COPYRIGHT AND LICENSE 250 | 251 | Copyright (C) 2011-2017, Viacheslav Tykhanovskyi 252 | 253 | This program is free software, you can redistribute it and/or modify it under 254 | the terms of the Artistic License version 2.0. 255 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is copyright (c) 2013 by Viacheslav Tykhanovskyi 2 | . 3 | 4 | This is free software; you can redistribute it and/or modify it under 5 | the same terms as the Perl 5 programming language system itself. 6 | 7 | Terms of the Perl programming language system itself 8 | 9 | a) the GNU General Public License as published by the Free 10 | Software Foundation; either version 1, or (at your option) any 11 | later version, or 12 | b) the "Artistic License" 13 | 14 | --- The GNU General Public License, Version 1, February 1989 --- 15 | 16 | This software is copyright (c) 2013 by Viacheslav Tykhanovskyi 17 | . 18 | 19 | This is free software, licensed under: 20 | 21 | The GNU General Public License, Version 1, February 1989 22 | 23 | GNU GENERAL PUBLIC LICENSE 24 | Version 1, February 1989 25 | 26 | Copyright (C) 1989 Free Software Foundation, Inc. 27 | 51 Franklin St, Suite 500, Boston, MA 02110-1335 USA 28 | 29 | Everyone is permitted to copy and distribute verbatim copies 30 | of this license document, but changing it is not allowed. 31 | 32 | Preamble 33 | 34 | The license agreements of most software companies try to keep users 35 | at the mercy of those companies. By contrast, our General Public 36 | License is intended to guarantee your freedom to share and change free 37 | software--to make sure the software is free for all its users. The 38 | General Public License applies to the Free Software Foundation's 39 | software and to any other program whose authors commit to using it. 40 | You can use it for your programs, too. 41 | 42 | When we speak of free software, we are referring to freedom, not 43 | price. Specifically, the General Public License is designed to make 44 | sure that you have the freedom to give away or sell copies of free 45 | software, that you receive source code or can get it if you want it, 46 | that you can change the software or use pieces of it in new free 47 | programs; and that you know you can do these things. 48 | 49 | To protect your rights, we need to make restrictions that forbid 50 | anyone to deny you these rights or to ask you to surrender the rights. 51 | These restrictions translate to certain responsibilities for you if you 52 | distribute copies of the software, or if you modify it. 53 | 54 | For example, if you distribute copies of a such a program, whether 55 | gratis or for a fee, you must give the recipients all the rights that 56 | you have. You must make sure that they, too, receive or can get the 57 | source code. And you must tell them their rights. 58 | 59 | We protect your rights with two steps: (1) copyright the software, and 60 | (2) offer you this license which gives you legal permission to copy, 61 | distribute and/or modify the software. 62 | 63 | Also, for each author's protection and ours, we want to make certain 64 | that everyone understands that there is no warranty for this free 65 | software. If the software is modified by someone else and passed on, we 66 | want its recipients to know that what they have is not the original, so 67 | that any problems introduced by others will not reflect on the original 68 | authors' reputations. 69 | 70 | The precise terms and conditions for copying, distribution and 71 | modification follow. 72 | 73 | GNU GENERAL PUBLIC LICENSE 74 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 75 | 76 | 0. This License Agreement applies to any program or other work which 77 | contains a notice placed by the copyright holder saying it may be 78 | distributed under the terms of this General Public License. The 79 | "Program", below, refers to any such program or work, and a "work based 80 | on the Program" means either the Program or any work containing the 81 | Program or a portion of it, either verbatim or with modifications. Each 82 | licensee is addressed as "you". 83 | 84 | 1. You may copy and distribute verbatim copies of the Program's source 85 | code as you receive it, in any medium, provided that you conspicuously and 86 | appropriately publish on each copy an appropriate copyright notice and 87 | disclaimer of warranty; keep intact all the notices that refer to this 88 | General Public License and to the absence of any warranty; and give any 89 | other recipients of the Program a copy of this General Public License 90 | along with the Program. You may charge a fee for the physical act of 91 | transferring a copy. 92 | 93 | 2. You may modify your copy or copies of the Program or any portion of 94 | it, and copy and distribute such modifications under the terms of Paragraph 95 | 1 above, provided that you also do the following: 96 | 97 | a) cause the modified files to carry prominent notices stating that 98 | you changed the files and the date of any change; and 99 | 100 | b) cause the whole of any work that you distribute or publish, that 101 | in whole or in part contains the Program or any part thereof, either 102 | with or without modifications, to be licensed at no charge to all 103 | third parties under the terms of this General Public License (except 104 | that you may choose to grant warranty protection to some or all 105 | third parties, at your option). 106 | 107 | c) If the modified program normally reads commands interactively when 108 | run, you must cause it, when started running for such interactive use 109 | in the simplest and most usual way, to print or display an 110 | announcement including an appropriate copyright notice and a notice 111 | that there is no warranty (or else, saying that you provide a 112 | warranty) and that users may redistribute the program under these 113 | conditions, and telling the user how to view a copy of this General 114 | Public License. 115 | 116 | d) You may charge a fee for the physical act of transferring a 117 | copy, and you may at your option offer warranty protection in 118 | exchange for a fee. 119 | 120 | Mere aggregation of another independent work with the Program (or its 121 | derivative) on a volume of a storage or distribution medium does not bring 122 | the other work under the scope of these terms. 123 | 124 | 3. You may copy and distribute the Program (or a portion or derivative of 125 | it, under Paragraph 2) in object code or executable form under the terms of 126 | Paragraphs 1 and 2 above provided that you also do one of the following: 127 | 128 | a) accompany it with the complete corresponding machine-readable 129 | source code, which must be distributed under the terms of 130 | Paragraphs 1 and 2 above; or, 131 | 132 | b) accompany it with a written offer, valid for at least three 133 | years, to give any third party free (except for a nominal charge 134 | for the cost of distribution) a complete machine-readable copy of the 135 | corresponding source code, to be distributed under the terms of 136 | Paragraphs 1 and 2 above; or, 137 | 138 | c) accompany it with the information you received as to where the 139 | corresponding source code may be obtained. (This alternative is 140 | allowed only for noncommercial distribution and only if you 141 | received the program in object code or executable form alone.) 142 | 143 | Source code for a work means the preferred form of the work for making 144 | modifications to it. For an executable file, complete source code means 145 | all the source code for all modules it contains; but, as a special 146 | exception, it need not include source code for modules which are standard 147 | libraries that accompany the operating system on which the executable 148 | file runs, or for standard header files or definitions files that 149 | accompany that operating system. 150 | 151 | 4. You may not copy, modify, sublicense, distribute or transfer the 152 | Program except as expressly provided under this General Public License. 153 | Any attempt otherwise to copy, modify, sublicense, distribute or transfer 154 | the Program is void, and will automatically terminate your rights to use 155 | the Program under this License. However, parties who have received 156 | copies, or rights to use copies, from you under this General Public 157 | License will not have their licenses terminated so long as such parties 158 | remain in full compliance. 159 | 160 | 5. By copying, distributing or modifying the Program (or any work based 161 | on the Program) you indicate your acceptance of this license to do so, 162 | and all its terms and conditions. 163 | 164 | 6. Each time you redistribute the Program (or any work based on the 165 | Program), the recipient automatically receives a license from the original 166 | licensor to copy, distribute or modify the Program subject to these 167 | terms and conditions. You may not impose any further restrictions on the 168 | recipients' exercise of the rights granted herein. 169 | 170 | 7. The Free Software Foundation may publish revised and/or new versions 171 | of the General Public License from time to time. Such new versions will 172 | be similar in spirit to the present version, but may differ in detail to 173 | address new problems or concerns. 174 | 175 | Each version is given a distinguishing version number. If the Program 176 | specifies a version number of the license which applies to it and "any 177 | later version", you have the option of following the terms and conditions 178 | either of that version or of any later version published by the Free 179 | Software Foundation. If the Program does not specify a version number of 180 | the license, you may choose any version ever published by the Free Software 181 | Foundation. 182 | 183 | 8. If you wish to incorporate parts of the Program into other free 184 | programs whose distribution conditions are different, write to the author 185 | to ask for permission. For software which is copyrighted by the Free 186 | Software Foundation, write to the Free Software Foundation; we sometimes 187 | make exceptions for this. Our decision will be guided by the two goals 188 | of preserving the free status of all derivatives of our free software and 189 | of promoting the sharing and reuse of software generally. 190 | 191 | NO WARRANTY 192 | 193 | 9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 194 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 195 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 196 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 197 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 198 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 199 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 200 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 201 | REPAIR OR CORRECTION. 202 | 203 | 10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 204 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 205 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 206 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 207 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 208 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 209 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 210 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 211 | POSSIBILITY OF SUCH DAMAGES. 212 | 213 | END OF TERMS AND CONDITIONS 214 | 215 | Appendix: How to Apply These Terms to Your New Programs 216 | 217 | If you develop a new program, and you want it to be of the greatest 218 | possible use to humanity, the best way to achieve this is to make it 219 | free software which everyone can redistribute and change under these 220 | terms. 221 | 222 | To do so, attach the following notices to the program. It is safest to 223 | attach them to the start of each source file to most effectively convey 224 | the exclusion of warranty; and each file should have at least the 225 | "copyright" line and a pointer to where the full notice is found. 226 | 227 | 228 | Copyright (C) 19yy 229 | 230 | This program is free software; you can redistribute it and/or modify 231 | it under the terms of the GNU General Public License as published by 232 | the Free Software Foundation; either version 1, or (at your option) 233 | any later version. 234 | 235 | This program is distributed in the hope that it will be useful, 236 | but WITHOUT ANY WARRANTY; without even the implied warranty of 237 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 238 | GNU General Public License for more details. 239 | 240 | You should have received a copy of the GNU General Public License 241 | along with this program; if not, write to the Free Software 242 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA 243 | 244 | 245 | Also add information on how to contact you by electronic and paper mail. 246 | 247 | If the program is interactive, make it output a short notice like this 248 | when it starts in an interactive mode: 249 | 250 | Gnomovision version 69, Copyright (C) 19xx name of author 251 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 252 | This is free software, and you are welcome to redistribute it 253 | under certain conditions; type `show c' for details. 254 | 255 | The hypothetical commands `show w' and `show c' should show the 256 | appropriate parts of the General Public License. Of course, the 257 | commands you use may be called something other than `show w' and `show 258 | c'; they could even be mouse-clicks or menu items--whatever suits your 259 | program. 260 | 261 | You should also get your employer (if you work as a programmer) or your 262 | school, if any, to sign a "copyright disclaimer" for the program, if 263 | necessary. Here a sample; alter the names: 264 | 265 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 266 | program `Gnomovision' (a program to direct compilers to make passes 267 | at assemblers) written by James Hacker. 268 | 269 | , 1 April 1989 270 | Ty Coon, President of Vice 271 | 272 | That's all there is to it! 273 | 274 | 275 | --- The Artistic License 1.0 --- 276 | 277 | This software is copyright (c) 2013 by Viacheslav Tykhanovskyi 278 | . 279 | 280 | This is free software, licensed under: 281 | 282 | The Artistic License 1.0 283 | 284 | The Artistic License 285 | 286 | Preamble 287 | 288 | The intent of this document is to state the conditions under which a Package 289 | may be copied, such that the Copyright Holder maintains some semblance of 290 | artistic control over the development of the package, while giving the users of 291 | the package the right to use and distribute the Package in a more-or-less 292 | customary fashion, plus the right to make reasonable modifications. 293 | 294 | Definitions: 295 | 296 | - "Package" refers to the collection of files distributed by the Copyright 297 | Holder, and derivatives of that collection of files created through 298 | textual modification. 299 | - "Standard Version" refers to such a Package if it has not been modified, 300 | or has been modified in accordance with the wishes of the Copyright 301 | Holder. 302 | - "Copyright Holder" is whoever is named in the copyright or copyrights for 303 | the package. 304 | - "You" is you, if you're thinking about copying or distributing this Package. 305 | - "Reasonable copying fee" is whatever you can justify on the basis of media 306 | cost, duplication charges, time of people involved, and so on. (You will 307 | not be required to justify it to the Copyright Holder, but only to the 308 | computing community at large as a market that must bear the fee.) 309 | - "Freely Available" means that no fee is charged for the item itself, though 310 | there may be fees involved in handling the item. It also means that 311 | recipients of the item may redistribute it under the same conditions they 312 | received it. 313 | 314 | 1. You may make and give away verbatim copies of the source form of the 315 | Standard Version of this Package without restriction, provided that you 316 | duplicate all of the original copyright notices and associated disclaimers. 317 | 318 | 2. You may apply bug fixes, portability fixes and other modifications derived 319 | from the Public Domain or from the Copyright Holder. A Package modified in such 320 | a way shall still be considered the Standard Version. 321 | 322 | 3. You may otherwise modify your copy of this Package in any way, provided that 323 | you insert a prominent notice in each changed file stating how and when you 324 | changed that file, and provided that you do at least ONE of the following: 325 | 326 | a) place your modifications in the Public Domain or otherwise make them 327 | Freely Available, such as by posting said modifications to Usenet or an 328 | equivalent medium, or placing the modifications on a major archive site 329 | such as ftp.uu.net, or by allowing the Copyright Holder to include your 330 | modifications in the Standard Version of the Package. 331 | 332 | b) use the modified Package only within your corporation or organization. 333 | 334 | c) rename any non-standard executables so the names do not conflict with 335 | standard executables, which must also be provided, and provide a separate 336 | manual page for each non-standard executable that clearly documents how it 337 | differs from the Standard Version. 338 | 339 | d) make other distribution arrangements with the Copyright Holder. 340 | 341 | 4. You may distribute the programs of this Package in object code or executable 342 | form, provided that you do at least ONE of the following: 343 | 344 | a) distribute a Standard Version of the executables and library files, 345 | together with instructions (in the manual page or equivalent) on where to 346 | get the Standard Version. 347 | 348 | b) accompany the distribution with the machine-readable source of the Package 349 | with your modifications. 350 | 351 | c) accompany any non-standard executables with their corresponding Standard 352 | Version executables, giving the non-standard executables non-standard 353 | names, and clearly documenting the differences in manual pages (or 354 | equivalent), together with instructions on where to get the Standard 355 | Version. 356 | 357 | d) make other distribution arrangements with the Copyright Holder. 358 | 359 | 5. You may charge a reasonable copying fee for any distribution of this 360 | Package. You may charge any fee you choose for support of this Package. You 361 | may not charge a fee for this Package itself. However, you may distribute this 362 | Package in aggregate with other (possibly commercial) programs as part of a 363 | larger (possibly commercial) software distribution provided that you do not 364 | advertise this Package as a product of your own. 365 | 366 | 6. The scripts and library files supplied as input to or produced as output 367 | from the programs of this Package do not automatically fall under the copyright 368 | of this Package, but belong to whomever generated them, and may be sold 369 | commercially, and may be aggregated with this Package. 370 | 371 | 7. C or perl subroutines supplied by you and linked into this Package shall not 372 | be considered part of this Package. 373 | 374 | 8. The name of the Copyright Holder may not be used to endorse or promote 375 | products derived from this software without specific prior written permission. 376 | 377 | 9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED 378 | WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF 379 | MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. 380 | 381 | The End 382 | -------------------------------------------------------------------------------- /lib/Text/Caml.pm: -------------------------------------------------------------------------------- 1 | package Text::Caml; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | require Carp; 7 | require Scalar::Util; 8 | use File::Spec (); 9 | 10 | our $VERSION = '0.17'; 11 | 12 | our $LEADING_SPACE = qr/(?:\n [ ]*)?/x; 13 | our $TRAILING_SPACE = qr/(?:[ ]* \n)?/x; 14 | our $START_TAG = qr/\{\{/x; 15 | our $END_TAG = qr/\}\}/x; 16 | 17 | our $START_OF_PARTIAL = quotemeta '>'; 18 | our $START_OF_SECTION = quotemeta '#'; 19 | our $START_OF_INVERTED_SECTION = quotemeta '^'; 20 | our $END_OF_SECTION = quotemeta '/'; 21 | our $START_OF_TEMPLATE_INHERITANCE = quotemeta '<'; 22 | our $END_OF_TEMPLATE_INHERITANCE = quotemeta '/'; 23 | our $START_OF_BLOCK = quotemeta '$'; 24 | our $END_OF_BLOCK = quotemeta '/'; 25 | 26 | sub new { 27 | my $class = shift; 28 | my (%params) = @_; 29 | 30 | my $self = {}; 31 | bless $self, $class; 32 | 33 | $self->{templates_path} = $params{templates_path}; 34 | $self->{default_partial_extension} = $params{default_partial_extension}; 35 | $self->{do_not_escape} = $params{do_not_escape}; 36 | 37 | $self->set_templates_path('.') 38 | unless $self->templates_path; 39 | 40 | return $self; 41 | } 42 | 43 | sub templates_path { $_[0]->{templates_path} } 44 | sub set_templates_path { $_[0]->{templates_path} = $_[1] } 45 | 46 | sub render { 47 | my $self = shift; 48 | my $template = shift; 49 | my $context = ref $_[0] eq 'HASH' ? $_[0] : {@_}; 50 | 51 | $self->_parse($template, $context); 52 | } 53 | 54 | sub render_file { 55 | my $self = shift; 56 | my $template = shift; 57 | my $context = ref $_[0] eq 'HASH' ? $_[0] : {@_}; 58 | 59 | $template = $self->_slurp_template($template); 60 | return $self->_parse($template, $context); 61 | } 62 | 63 | sub _parse { 64 | my $self = shift; 65 | my $template = shift; 66 | my $context = shift; 67 | my $override = shift; 68 | 69 | my $output = ''; 70 | 71 | pos $template = 0; 72 | while (pos $template < length $template) { 73 | if ($template =~ m/($LEADING_SPACE)?\G $START_TAG /gcxms) { 74 | my $chunk = ''; 75 | 76 | my $leading_newline = !!$1; 77 | 78 | # Tripple 79 | if ($template =~ m/\G \{ (.*?) \} $END_TAG/gcxms) { 80 | $chunk .= $self->_parse_tag($1, $context); 81 | } 82 | 83 | # Replace 84 | elsif ($template =~ m/\G - (.*?) $END_TAG/gcxms) { 85 | $chunk .= '{{' . $1 . '}}'; 86 | } 87 | 88 | # Comment 89 | elsif ($template =~ m/\G ! .*? $END_TAG/gcxms) { 90 | } 91 | 92 | # Section 93 | elsif ($template =~ m/\G $START_OF_SECTION \s* (.*?) \s* $END_TAG ($TRAILING_SPACE)?/gcxms) { 94 | my $name = $1; 95 | my $end_of_section = $name; 96 | 97 | if ( 98 | $template =~ m/ 99 | \G 100 | (.*?) 101 | ($LEADING_SPACE)? 102 | $START_TAG 103 | $END_OF_SECTION 104 | $end_of_section 105 | $END_TAG 106 | ($TRAILING_SPACE)? 107 | /gcxms 108 | ) 109 | { 110 | $chunk .= $self->_parse_section($name, $1, $context); 111 | } 112 | else { 113 | Carp::croak("Section's '$name' end not found"); 114 | } 115 | } 116 | 117 | # Inverted section 118 | elsif ($template =~ m/\G $START_OF_INVERTED_SECTION (.*?) $END_TAG ($TRAILING_SPACE)?/gcxms) { 119 | my $name = $1; 120 | 121 | if ($template =~ 122 | m/ \G (.*?) ($LEADING_SPACE)? $START_TAG $END_OF_SECTION $name $END_TAG ($TRAILING_SPACE)?/gcxms) 123 | { 124 | $chunk .= $self->_parse_inverted_section($name, $1, $context); 125 | } 126 | else { 127 | Carp::croak("Section's '$name' end not found"); 128 | } 129 | } 130 | 131 | # End of section 132 | elsif ($template =~ m/\G $END_OF_SECTION (.*?) $END_TAG/gcxms) { 133 | Carp::croak("Unexpected end of section '$1'"); 134 | } 135 | 136 | # Partial 137 | elsif ($template =~ m/\G $START_OF_PARTIAL \s* (.*?) \s* $END_TAG/gcxms) { 138 | $chunk .= $self->_parse_partial($1, $context); 139 | } 140 | 141 | # Inherited template 142 | elsif ($template =~ m/\G $START_OF_TEMPLATE_INHERITANCE \s* (.*?) \s* $END_TAG/gcxms) { 143 | my $name = $1; 144 | my $end_of_inherited_template = $name; 145 | 146 | if ( 147 | $template =~ m/ 148 | \G (.*?) 149 | ($LEADING_SPACE)? 150 | $START_TAG 151 | $END_OF_TEMPLATE_INHERITANCE 152 | $end_of_inherited_template 153 | $END_TAG 154 | ($TRAILING_SPACE)? 155 | /gcxms 156 | ) 157 | { 158 | $chunk .= $self->_parse_inherited_template($name, $1, $context); 159 | } 160 | else { 161 | Carp::croak("Nested template's '$name' end not found"); 162 | } 163 | } 164 | 165 | # block 166 | elsif ($template =~ m/\G $START_OF_BLOCK \s* (.*?) \s* $END_TAG/gcxms) { 167 | my $name = $1; 168 | my $end_of_block = $name; 169 | 170 | if ($template =~ m/\G (.*?) ($LEADING_SPACE)? $START_TAG $END_OF_BLOCK $end_of_block $END_TAG/gcxms) { 171 | $chunk .= $self->_parse_block($name, $1, $context, $override); 172 | } 173 | else { 174 | Carp::croak("Block's '$name' end not found"); 175 | } 176 | } 177 | 178 | # Tag 179 | elsif ($template =~ m/\G (.*?) $END_TAG/gcxms) { 180 | $chunk .= $self->_parse_tag_escaped($1, $context); 181 | } 182 | else { 183 | Carp::croak("Can't find where tag is closed"); 184 | } 185 | 186 | if ($chunk ne '') { 187 | $output .= $chunk; 188 | } 189 | elsif ($output eq '' || $leading_newline) { 190 | if ($template =~ m/\G $TRAILING_SPACE/gcxms) { 191 | $output =~ s/[ ]*\z//xms; 192 | } 193 | } 194 | } 195 | 196 | # Text before tag 197 | elsif ($template =~ m/\G (.*?) (?=$START_TAG\{?)/gcxms) { 198 | $output .= $1; 199 | } 200 | 201 | # Other text 202 | else { 203 | $output .= substr($template, pos($template)); 204 | last; 205 | } 206 | } 207 | 208 | return $output; 209 | } 210 | 211 | sub _parse_tag { 212 | my $self = shift; 213 | my ($name, $context) = @_; 214 | 215 | my $value; 216 | my %args; 217 | 218 | # Current element 219 | if ($name eq '.') { 220 | return '' if $self->_is_empty($context, $name); 221 | 222 | $value = $context->{$name}; 223 | } 224 | 225 | else { 226 | $value = $self->_get_value($context, $name); 227 | } 228 | 229 | if (ref $value eq 'CODE') { 230 | my $content = $value->($self, '', $context); 231 | $content = '' unless defined $content; 232 | return $self->_parse($content, $context); 233 | } 234 | 235 | return $value; 236 | } 237 | 238 | sub _find_value { 239 | my $self = shift; 240 | my ($context, $name) = @_; 241 | 242 | my @parts = split /\./ => $name; 243 | 244 | my $value = $context; 245 | 246 | foreach my $part (@parts) { 247 | if ( ref $value eq "HASH" 248 | && exists $value->{'_with'} 249 | && Scalar::Util::blessed($value->{'_with'}) 250 | && $value->{'_with'}->can($part)) 251 | { 252 | $value = $value->{'_with'}->$part; 253 | next; 254 | } 255 | 256 | if (ref $value eq "ARRAY") { 257 | $value = $value->[$part]; 258 | next; 259 | } 260 | 261 | if ( exists $value->{'.'} 262 | && Scalar::Util::blessed($value->{'.'}) 263 | && $value->{'.'}->can($part)) 264 | { 265 | $value = $value->{'.'}->$part; 266 | next; 267 | } 268 | 269 | return undef if $self->_is_empty($value, $part); 270 | $value = Scalar::Util::blessed($value) ? $value->$part : $value->{$part}; 271 | } 272 | 273 | return \$value; 274 | } 275 | 276 | sub _get_value { 277 | my $self = shift; 278 | my ($context, $name) = @_; 279 | 280 | if ($name eq '.') { 281 | return '' if $self->_is_empty($context, $name); 282 | return $context->{$name}; 283 | } 284 | 285 | my $value = $self->_find_value($context, $name); 286 | 287 | return $value ? $$value : ''; 288 | } 289 | 290 | sub _parse_tag_escaped { 291 | my $self = shift; 292 | my ($tag, $context) = @_; 293 | 294 | my $do_not_escape = $self->{do_not_escape}; 295 | if ($tag =~ s/\A \&//xms) { 296 | $do_not_escape = !$do_not_escape; 297 | } 298 | 299 | my $output = $self->_parse_tag($tag, $context); 300 | 301 | $output = $self->_escape($output) unless $do_not_escape; 302 | 303 | return $output; 304 | } 305 | 306 | sub _parse_section { 307 | my $self = shift; 308 | my ($name, $template, $context) = @_; 309 | 310 | my $value = $self->_get_value($context, $name); 311 | 312 | my $output = ''; 313 | 314 | if (ref $value eq 'HASH') { 315 | $output .= $self->_parse($template, {%$context, %$value}); 316 | } 317 | elsif (ref $value eq 'ARRAY') { 318 | my $idx = 0; 319 | foreach my $el (@$value) { 320 | my %subcontext = ref $el eq 'HASH' ? %$el : ('.' => $el); 321 | $subcontext{'_idx'} = $idx; 322 | 323 | $subcontext{'_even'} = $idx % 2 == 0; 324 | $subcontext{'_odd'} = $idx % 2 != 0; 325 | 326 | $subcontext{'_first'} = $idx == 0; 327 | $subcontext{'_last'} = $idx == $#$value; 328 | 329 | $output .= $self->_parse($template, {%$context, %subcontext}); 330 | 331 | $idx++; 332 | } 333 | } 334 | elsif (ref $value eq 'CODE') { 335 | $template = $self->_parse($template, $context); 336 | $output .= $self->_parse($value->($self, $template, $context), $context); 337 | } 338 | elsif (ref $value) { 339 | $output .= $self->_parse($template, {%$context, _with => $value}); 340 | } 341 | elsif ($value) { 342 | $output .= $self->_parse($template, $context); 343 | } 344 | 345 | return $output; 346 | } 347 | 348 | sub _parse_inverted_section { 349 | my $self = shift; 350 | my ($name, $template, $context) = @_; 351 | 352 | my $value = $self->_find_value($context, $name); 353 | return $self->_parse($template, $context) 354 | unless defined $value; 355 | 356 | $value = $$value; 357 | my $output = ''; 358 | 359 | if (ref $value eq 'HASH') { 360 | } 361 | elsif (ref $value eq 'ARRAY') { 362 | return '' if @$value; 363 | 364 | $output .= $self->_parse($template, $context); 365 | } 366 | elsif (!$value) { 367 | $output .= $self->_parse($template, $context); 368 | } 369 | 370 | return $output; 371 | } 372 | 373 | sub _parse_partial { 374 | my $self = shift; 375 | my ($template, $context) = @_; 376 | 377 | if (my $ext = $self->{default_partial_extension}) { 378 | $template = "$template.$ext"; 379 | } 380 | 381 | my $parse = 1; 382 | if ($template =~ s{^\&}{}) { 383 | $parse = 0; 384 | } 385 | 386 | my $content = $self->_slurp_template($template); 387 | 388 | return $parse ? $self->_parse($content, $context) : $content; 389 | } 390 | 391 | sub _parse_inherited_template { 392 | my $self = shift; 393 | my ($name, $override, $context) = @_; 394 | 395 | if (my $ext = $self->{default_partial_extension}) { 396 | $name = "$name.$ext"; 397 | } 398 | 399 | my $content = $self->_slurp_template($name); 400 | 401 | return $self->_parse($content, $context, $override); 402 | } 403 | 404 | sub _parse_block { 405 | my $self = shift; 406 | my ($name, $template, $context, $override) = @_; 407 | 408 | # get block content from override 409 | my $content; 410 | 411 | # first, see if we can find any starting block with this name in the override 412 | if ($override =~ m/ $START_OF_BLOCK \s* $name \s* $END_TAG/gcxms) { 413 | 414 | # get the content of the override block and make sure there's a corresponding end-block tag for it! 415 | if ($override =~ m/ (.*) $START_TAG $END_OF_BLOCK \s* $name \s* $END_TAG/gcxms) { 416 | my $content = $1; 417 | return $self->_parse($content, $context); 418 | } 419 | else { 420 | Carp::croak("Block's '$name' end not found"); 421 | } 422 | } 423 | 424 | return $self->_parse($template, $context); 425 | } 426 | 427 | sub _slurp_template { 428 | my $self = shift; 429 | my ($template) = @_; 430 | 431 | my $path = 432 | defined $self->templates_path && !(File::Spec->file_name_is_absolute($template)) 433 | ? File::Spec->catfile($self->templates_path, $template) 434 | : $template; 435 | 436 | Carp::croak("Can't find '$path'") unless defined $path && -f $path; 437 | 438 | my $content = do { 439 | local $/; 440 | open my $file, '<:encoding(UTF-8)', $path or return; 441 | <$file>; 442 | }; 443 | 444 | Carp::croak("Can't open '$template'") unless defined $content; 445 | 446 | chomp $content; 447 | 448 | return $content; 449 | } 450 | 451 | sub _is_empty { 452 | my $self = shift; 453 | my ($vars, $name) = @_; 454 | 455 | my $var; 456 | 457 | if (Scalar::Util::blessed($vars)) { 458 | $var = $vars->$name; 459 | } 460 | else { 461 | return 1 unless exists $vars->{$name}; 462 | $var = $vars->{$name}; 463 | } 464 | 465 | return 1 unless defined $var; 466 | return 1 if $var eq ''; 467 | 468 | return 0; 469 | } 470 | 471 | sub _escape { 472 | my $self = shift; 473 | my $value = shift; 474 | 475 | $value =~ s/&/&/g; 476 | $value =~ s//>/g; 478 | $value =~ s/"/"/g; 479 | 480 | return $value; 481 | } 482 | 483 | 1; 484 | __END__ 485 | 486 | =head1 NAME 487 | 488 | Text::Caml - Mustache template engine 489 | 490 | =head1 SYNOPSIS 491 | 492 | my $view = Text::Caml->new; 493 | 494 | my $output = $view->render_file('template', {title => 'Hello', body => 'there!'}); 495 | 496 | # template 497 | 498 | 499 | {{title}} 500 | 501 | 502 | {{body}} 503 | 504 | 505 | 506 | $output = $view->render('{{hello}}', {hello => 'hi'}); 507 | 508 | =head1 DESCRIPTION 509 | 510 | L is a Mustache-like (L) template engine. 511 | That means it tends to have no logic in template files. 512 | 513 | =head2 Syntax 514 | 515 | =head3 Context 516 | 517 | Context is the data passed to the template. Context can change during template 518 | rendering and be specific in various cases. 519 | 520 | =head3 Variables 521 | 522 | Variables are inserted using C<{{foo}}> syntax. If a variable is not defined or 523 | empty it is simply ignored. 524 | 525 | Hello {{user}}! 526 | 527 | By default every variable is escaped when parsed. This can be omitted using C<&> 528 | flag. 529 | 530 | # user is '1 > 2' 531 | Hello {{user}}! => Hello 1 > 2! 532 | 533 | Hello {{&user}}! => Hello 1 > 2! 534 | 535 | Using a C<.> syntax it is possible to access deep hash structures. 536 | 537 | # user => {name => 'Larry'} 538 | {{user.name}} 539 | 540 | Larry 541 | 542 | =head3 Comments 543 | 544 | Comments are ignored. They can be multiline too. 545 | 546 | foo{{! Comment}}bar 547 | 548 | foo{{! 549 | Comment 550 | }}bar 551 | 552 | =head3 Sections 553 | 554 | Sections are like iterators that iterate over your data. Depending on a 555 | variable type different iterators are created. 556 | 557 | =over 4 558 | 559 | =item * 560 | 561 | Boolean, C is defined, not zero and not empty. 562 | 563 | # have_comments => 1 564 | {{#have_comments}} 565 | We have comments! 566 | {{/have_comments}} 567 | 568 | We have comments! 569 | 570 | =item * 571 | 572 | Array, C is a non-empty array reference. Special variable C<{{.}}> is 573 | created to point to the current element. 574 | 575 | # list => [1, 2, 3] 576 | {{#list}}{{.}}{{/list}} 577 | 578 | 123 579 | 580 | =item * 581 | 582 | Hash, C is a non-empty hash reference. Context is swithed to the 583 | elements. 584 | 585 | # hash => {one => 1, two => 2, three => 3} 586 | {{#hash}} 587 | {{one}}{{two}}{{three}} 588 | {{/hash}} 589 | 590 | 123 591 | 592 | =item * 593 | 594 | Lambda, C is an anonymous subroutine, that's called with three 595 | arguments: current object instance, template and the context. This can be used 596 | for subrendering, helpers etc. 597 | 598 | wrapped => sub { 599 | my $self = shift; 600 | my $text = shift; 601 | 602 | return '' . $self->render($text, @_) . ''; 603 | }; 604 | 605 | {{#wrapped}} 606 | {{name}} is awesome. 607 | {{/wrapped}} 608 | 609 | Willy is awesome. 610 | 611 | =back 612 | 613 | =head3 Inverted sections 614 | 615 | Inverted sections are run in those situations when normal sections don't. When 616 | boolean value is false, array is empty etc. 617 | 618 | # repo => [] 619 | {{#repo}} 620 | {{name}} 621 | {{/repo}} 622 | {{^repo}} 623 | No repos :( 624 | {{/repo}} 625 | 626 | No repos :( 627 | 628 | =head3 Partials 629 | 630 | Partials are like C in other templates engines. They are run with the 631 | current context and can be recursive. 632 | 633 | {{#articles}} 634 | {{>article_summary}} 635 | {{/articles}} 636 | 637 | If you want to include another file without parsing, prefix the filename with C<&>: 638 | 639 | {{#articles}} 640 | {{>&article_summary_not_parsed}} 641 | {{/articles}} 642 | 643 | =head3 Nested Templates 644 | 645 | This gives horgan.js style template inheritance. 646 | 647 | {{! header.mustache }} 648 | 649 | {{$title}}Default title{{/title}} 650 | 651 | 652 | {{! base.mustache }} 653 | 654 | {{$header}}{{/header}} 655 | {{$content}}{{/content}} 656 | 657 | 658 | {{! mypage.mustache }} 659 | {{Hello world 668 | {{/content}} 669 | {{/base}} 670 | 671 | Rendering mypage.mustache would output: 672 | My page title

Hello world

673 | 674 | =cut 675 | 676 | =head1 ATTRIBUTES 677 | 678 | =head2 C 679 | 680 | my $path = $engine->templates_path; 681 | 682 | Return path where templates are searched. 683 | 684 | =head2 C 685 | 686 | my $path = $engine->set_templates_path('templates'); 687 | 688 | Set base path under which templates are searched. 689 | 690 | =head2 C 691 | 692 | If this option is set that the extension is automatically added to the partial 693 | filenames. 694 | 695 | my $engine = Text::Caml->new(default_partial_extension => 'caml'); 696 | 697 | --- 698 | {{#articles}} 699 | {{>article_summary}} # article_summary.caml will be searched 700 | {{/articles}} 701 | 702 | =head1 METHODS 703 | 704 | =head2 C 705 | 706 | my $engine = Text::Caml->new; 707 | 708 | Create a new L object. 709 | 710 | =head2 C 711 | 712 | $engine->render('{{foo}}', {foo => 'bar'}); 713 | 714 | Render template from string. 715 | 716 | =head2 C 717 | 718 | $engine->render_file('template.mustache', {foo => 'bar'}); 719 | 720 | Render template from file. 721 | 722 | =head1 DEVELOPMENT 723 | 724 | =head2 Repository 725 | 726 | http://github.com/vti/text-caml 727 | 728 | =head1 AUTHOR 729 | 730 | Viacheslav Tykhanovskyi, C 731 | 732 | =head1 CREDITS 733 | 734 | Sergey Zasenko (und3f) 735 | 736 | Andrew Rodland (arodland) 737 | 738 | Alex Balhatchet (kaoru) 739 | 740 | Yves Chevallier 741 | 742 | Ovidiu Stateina 743 | 744 | Fernando Oliveira 745 | 746 | Shoichi Kaji (skaji) 747 | 748 | Igor Lobanov 749 | 750 | =head1 COPYRIGHT AND LICENSE 751 | 752 | Copyright (C) 2011-2017, Viacheslav Tykhanovskyi 753 | 754 | This program is free software, you can redistribute it and/or modify it under 755 | the terms of the Artistic License version 2.0. 756 | 757 | =cut 758 | --------------------------------------------------------------------------------