File Coverage

blib/lib/Dancer2/Template/TemplateFlute.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Dancer2::Template::TemplateFlute;
2              
3             =head1 NAME
4              
5             Dancer2::Template::TemplateFlute - Template::Flute wrapper for Dancer2
6              
7             =head1 VERSION
8              
9             Version 0.203
10              
11             =cut
12              
13             our $VERSION = '0.203';
14              
15 1     1   46749 use Carp qw/croak/;
  1         2  
  1         43  
16 1     1   477 use Dancer2::Core::Types;
  1         7811  
  1         329  
17 1     1   9 use Module::Runtime qw/use_module/;
  1         5  
  1         5  
18 1     1   36 use Scalar::Util qw/blessed/;
  1         1  
  1         34  
19 1     1   688 use Template::Flute;
  0            
  0            
20             use Template::Flute::Iterator;
21             use Template::Flute::Utils;
22             use Template::Flute::I18N;
23             use Try::Tiny;
24              
25             use Moo;
26             with 'Dancer2::Core::Role::Template';
27             use namespace::clean;
28              
29             has autodetect_disable => (
30             is => 'ro',
31             isa => ArrayRef,
32             lazy => 1,
33             default => sub {
34             my $self = shift;
35             # do we need to add anything to default list?
36             my @autodetect_disable = ();
37             if ( my $autodetect = $self->config->{autodetect} ) {
38             push @autodetect_disable, @{ $autodetect->{disable} };
39             }
40             return \@autodetect_disable;
41             },
42             );
43              
44             has check_dangling => (
45             is => 'ro',
46             isa => Bool,
47             lazy => 1,
48             default => sub { shift->config->{check_dangling} },
49             );
50              
51             has disable_check_dangling => (
52             is => 'ro',
53             isa => Bool,
54             lazy => 1,
55             default => sub { shift->config->{disable_check_dangling} },
56             );
57              
58             has '+default_tmpl_ext' => (
59             default => sub { shift->config->{extension} || 'html' },
60             );
61              
62             has filters => (
63             is => 'ro',
64             isa => HashRef,
65             lazy => 1,
66             default => sub { shift->config->{filters} || +{} },
67             );
68              
69             has iterators => (
70             is => 'rw',
71             isa => HashRef,
72             lazy => 1,
73             default => sub { shift->config->{iterators} || +{} },
74             );
75              
76             has i18n_obj => (
77             is => 'lazy',
78             isa => InstanceOf['Template::Flute::I18N'],
79             );
80              
81             sub _build_i18n_obj {
82             my $self = shift;
83             my $conf = $self->config;
84             my $localize;
85             if ( $conf and exists $conf->{i18n} and exists $conf->{i18n}->{class} ) {
86             my $class = $conf->{i18n}->{class};
87             my %args;
88             if ( $conf->{i18n}->{options} ) {
89              
90             # do a shallow copy and pass that
91             %args = %{ $conf->{i18n}->{options} };
92             }
93             my $obj = try {
94             use_module($class)->new(%args);
95             }
96             catch {
97             croak "Failed to import class $class: $_";
98             };
99             my $method = $conf->{i18n}->{method} || 'localize';
100              
101             # store the closure in the object to avoid loading it up each time
102             $localize = sub {
103             my $to_translate = shift;
104             return $obj->$method($to_translate);
105             };
106             }
107              
108             # provide a common interface with Template::Flute::I18N
109             return Template::Flute::I18N->new($localize);
110             }
111              
112             sub render ($$$) {
113             my ( $self, $template, $tokens ) = @_;
114             my ( %args, $flute, $html, $name, %parms, %template_iterators,
115             %iterators, $class );
116              
117             %args = (
118             template_file => $template,
119             scopes => 1,
120             auto_iterators => 1,
121             values => $tokens,
122             filters => $self->filters,
123             autodetect => { disable => $self->autodetect_disable },
124             #autodetect => { disable => [qw/Dancer2::Session::Abstract/] },
125             );
126              
127             # determine whether we need to pass an adjust URI to Template::Flute
128             if ( my $request = $tokens->{request} ) {
129             $args{uri} = $request->base->path if $request->base->path ne '/';
130             }
131              
132             if ( my $i18n = $self->i18n_obj ) {
133             $args{i18n} = $i18n;
134             }
135              
136             if ( my $email_cids = $tokens->{email_cids} ) {
137             $args{email_cids} = $email_cids;
138              
139             # use the 'cids' tokens only if email_cids is defined
140             if ( my $cid_options = $tokens->{cids} ) {
141             $args{cids} = {%$cid_options};
142             }
143             }
144              
145             $flute = Template::Flute->new(%args);
146              
147             # process HTML template to determine iterators used by template
148             $flute->process_template();
149              
150             # instantiate iterators where object isn't yet available
151             if ( %template_iterators = $flute->template()->iterators ) {
152             my $selector;
153              
154             for my $name ( keys %template_iterators ) {
155             if ( my $value = $self->iterators->{$name} ) {
156             %parms = %$value;
157              
158             $class = "Template::Flute::Iterator::$parms{class}";
159              
160             if ( $parms{file} ) {
161             $parms{file} =
162             Template::Flute::Utils::derive_filename( $template,
163             $parms{file}, 1 );
164             }
165              
166             if ( $selector = delete $parms{selector} ) {
167             if ( $selector eq '*' ) {
168             $parms{selector} = '*';
169             }
170             elsif ( $tokens->{$selector} ) {
171             $parms{selector} =
172             { $selector => $tokens->{$selector} };
173             }
174             }
175              
176             eval "require $class";
177             if ($@) {
178             croak "Failed to load class $class for iterator $name: $@\n";
179             }
180              
181             eval { $iterators{$name} = $class->new(%parms); };
182              
183             if ($@) {
184             croak "Failed to instantiate class $class for iterator $name: $@\n";
185             }
186              
187             $flute->specification->set_iterator( $name, $iterators{$name} );
188             }
189             }
190             }
191              
192             # check for forms
193             if ( my @forms = $flute->template->forms() ) {
194             if ( $tokens->{form} ) {
195             $self->_tf_manage_forms( $flute, $tokens, @forms );
196             }
197             else {
198             $self->log_cb->( 'debug',
199             'Missing form parameters for forms '
200             . join( ", ", sort map { $_->name } @forms ) );
201             }
202             }
203             elsif ( $tokens->{form} ) {
204             my $form_name =
205             blessed( $tokens->{form} ) ? $tokens->{form}->name : $tokens->{form};
206              
207             $self->log_cb->( 'debug',
208             "Form $form_name passed, "
209             . "but no forms found in the template $template." );
210             }
211              
212             $html = $flute->process();
213              
214             if (
215             $self->check_dangling
216             or ( $tokens->{settings}->{environment} eq 'development'
217             && !$self->disable_check_dangling )
218             )
219             {
220              
221             if ( my @warnings = $flute->specification->dangling ) {
222             foreach my $warn (@warnings) {
223             $self->log_cb->(
224             'debug',
225             'Found dangling element '
226             . $warn->{type} . ' '
227             . $warn->{name} . ' (',
228             $warn->{dump},
229             ')'
230             );
231             }
232             }
233             }
234             return $html;
235             }
236              
237             sub _tf_manage_forms {
238             my ( $self, $flute, $tokens, @forms ) = @_;
239              
240             # simple case: only one form passed and one in the flute
241             if ( ref( $tokens->{form} ) ne 'ARRAY' ) {
242             my $form_name = $tokens->{form}->name;
243             if ( @forms == 1 ) {
244             my $form = shift @forms;
245             if ( $form_name eq 'main'
246             or $form_name eq $form->name )
247             {
248             $self->_tf_fill_forms( $flute, $tokens->{form}, $form,
249             $tokens );
250             }
251             }
252             else {
253             my $found = 0;
254             foreach my $form (@forms) {
255             if ( $form_name eq $form->name ) {
256             $self->_tf_fill_forms( $flute, $tokens->{form}, $form,
257             $tokens );
258             $found++;
259             }
260             }
261             if ( $found != 1 ) {
262             $self->log_cb->(
263             'error', "Multiple form are not being managed correctly, found $found corresponding forms, but we expected just one!"
264             );
265             }
266             }
267             }
268             else {
269             foreach my $passed_form ( @{ $tokens->{form} } ) {
270             foreach my $form (@forms) {
271             if ( $passed_form->name eq $form->name ) {
272             $self->_tf_fill_forms( $flute, $passed_form, $form,
273             $tokens );
274             }
275             }
276             }
277             }
278             }
279              
280             sub _tf_fill_forms {
281             my ( $self, $flute, $passed_form, $form, $tokens ) = @_;
282              
283             # arguments:
284             # $flute is the template object.
285              
286             # $passed_form is the Dancer::Plugin::Form object we got from the
287             # tokens, which is $tokens->{form} when we have just a single one.
288              
289             # $form is the form object we got from the template itself, with
290             # $flute->template->forms
291              
292             # $tokens is the hashref passed to the template. We need it for the
293             # iterators.
294              
295             my ( $iter, $action );
296             for my $name ( $form->iterators ) {
297             if ( ref( $tokens->{$name} ) eq 'ARRAY' ) {
298             $iter = Template::Flute::Iterator->new( $tokens->{$name} );
299             $flute->specification->set_iterator( $name, $iter );
300             }
301             }
302             if ( $action = $passed_form->action() ) {
303             $form->set_action($action);
304             }
305             $passed_form->set_fields( [ map { $_->{name} } @{ $form->fields() } ] );
306             $form->fill( $passed_form->values );
307              
308             $passed_form->to_session;
309             }
310              
311             =head1 DESCRIPTION
312              
313             This class is an interface between Dancer2's template engine abstraction layer
314             and the L module.
315              
316             In order to use this engine, use the template setting:
317              
318             template: template_flute
319              
320             The default template extension is ".html".
321              
322             =head2 LAYOUT
323              
324             Each layout needs a specification file and a template file. To embed
325             the content of your current view into the layout, put the following
326             into your specification file, e.g. F:
327              
328            
329            
330            
331              
332             This replaces the contents of the following block in your HTML
333             template, e.g. F:
334              
335            
336             Your content
337            
338              
339             =head2 ITERATORS
340              
341             Iterators can be specified explicitly in the configuration file as below.
342              
343             engines:
344             template:
345             template_flute:
346             iterators:
347             fruits:
348             class: JSON
349             file: fruits.json
350              
351             =head2 FILTER OPTIONS
352              
353             Filter options and classes can be specified in the configuration file as below.
354              
355             engines:
356             template:
357             template_flute:
358             filters:
359             currency:
360             options:
361             int_curr_symbol: "$"
362             image:
363             class: "Flowers::Filters::Image"
364              
365             =head2 ADJUSTING URIS
366              
367             We automatically adjust links in the templates if the value of
368             Cpath> is different from Cpath_info>.
369              
370             =head2 EMBEDDING IMAGES IN EMAILS
371              
372             If you pass a value named C, which should be an empty hash
373             reference, all the images C attributes will be rewritten using
374             the CIDs, and the reference will be populated with an hashref, as
375             documented in L
376              
377             Further options for the CIDs should be passed in an optional value
378             named C. See L for them.
379              
380              
381             =head2 DISABLE OBJECT AUTODETECTION
382              
383             Sometimes you want to pass values to a template which are objects, but
384             don't have an accessor, so they should be treated like hashrefs instead.
385              
386             You can specify classes with the following syntax:
387              
388             engines:
389             template:
390             template_flute:
391             autodetect:
392             disable:
393             - My::Class1
394             - My::Class2
395              
396              
397             The class matching is checked by L with C, so
398             any parent class would do.
399              
400             =head2 LOCALIZATION
401              
402             Templates can be localized using the Template::Flute::I18N module. You
403             can define a class that provides a method which takes as first (and
404             only argument) the string to translate, and returns the translated
405             one. You have to provide the class and the method. If the class is not
406             provided, no localization is done. If no method is specified,
407             'localize' will be used. The app will crash if the class doesn't
408             provide such method.
409              
410             B
411             translate the string>.
412              
413             Example configuration, assuming the class C provides a
414             C method.
415              
416             engines:
417             template:
418             template_flute:
419             i18n:
420             class: MyApp::Lexicon
421             method: try_to_translate
422              
423              
424             A class could be something like this:
425              
426             package MyTestApp::Lexicon;
427             use Dancer2;
428              
429             sub new {
430             my $class = shift;
431             debug "Loading up $class";
432             my $self = {
433             dictionary => {
434             en => {
435             'TRY' => 'Try',
436             },
437             it => {
438             'TRY' => 'Prova',
439             },
440             }
441             };
442             bless $self, $class;
443             }
444              
445             sub dictionary {
446             return shift->{dictionary};
447             }
448              
449             sub try_to_translate {
450             my ($self, $string) = @_;
451             my $lang = session('lang') || var('lang');
452             return $string unless $lang;
453             return $string unless $self->dictionary->{$lang};
454             my $tr = $self->dictionary->{$lang}->{$string};
455             defined $tr ? return $tr : return $string;
456             }
457              
458             1;
459              
460             Optionally, you can pass the options to instantiate the class in the
461             configuration. Like this:
462              
463             engines:
464             template:
465             template_flute:
466             i18n:
467             class: MyApp::Lexicon
468             method: localize
469             options:
470             append: 'X'
471             prepend: 'Y'
472             lexicon: 'path/to/po/files'
473              
474             This will call
475              
476             MyApp::Lexicon->new(append => 'X', prepend => 'Y', lexicon => 'path/to/po/files');
477              
478             when the engine is initialized, and will call the C method
479             on it to get the translations.
480              
481             =head2 DEBUG TOOLS
482              
483             If you set C in the engine stanza, the specification
484             will run a check (using the L's
485             C method) against the template to see if you have elements
486             of the specifications which are not bound to any HTML elements.
487              
488             In this case a debug message is issued (so keep in mind that with
489             higher logging level you are not going to see it).
490              
491             Example configuration:
492              
493             engines:
494             template:
495             template_flute:
496             check_dangling: 1
497              
498             When the environment is set to C this feature is turned
499             on by default. You can silence the logs by setting:
500              
501             engines:
502             template:
503             template_flute:
504             disable_check_dangling: 1
505              
506             =head2 FORMS
507              
508             Dancers::Template::TemplateFlute has a form plugin
509             L which must be installed in order to use
510             L forms.
511              
512             The token C
is reserved for forms. It can be a single
513             L form object or an arrayref of
514             L form objects.
515              
516             =head3 Typical usage for a single form.
517              
518             =head4 XML Specification
519              
520            
521            
522            
523            
524            
525            
526            
527              
528             =head4 HTML
529              
530            
531            
532            
Info
533            
534            
  • 535            
    536            
    537            
    538            
  • 539            
    540            
    541            
    542            
  • 543            
    544            
    545            
    546            
  • 547            
    548            
    549            
    550            
    551            
    552              
    553             =head4 Code
    554              
    555             any [qw/get post/] => '/register' => sub {
    556             my $form = request->is_post
    557             ? form('registration', source => 'body')
    558             : form('registration', source => 'session' );
    559             my %values = %{$form->values};
    560             # VALIDATE, filter, etc. the values
    561             template register => {form => $form };
    562             };
    563              
    564             =head3 Usage example for multiple forms
    565              
    566             =head4 Specification
    567              
    568            
    569            
    570            
    571            
    572            
    573            
    574            
    575            
    576            
    577            
    578            
    579              
    580             =head4 HTML
    581              
    582            

    Register

    583            
    584            
    585            
    Info
    586            
    587            
  • 588            
    589            
    590            
    591            
  • 592            
    593            
    594            
    595            
  • 596            
    597            
    598            
    599            
  • 600            
    601            
    602            
    603            
    604            
    605            

    Login

    606            
    607            
    608            
    Info
    609            
    610            
  • 611            
    612            
    613            
    614            
  • 615            
    616            
    617            
    618            
  • 619            
    620            
    621            
    622            
    623            
    624              
    625              
    626             =head4 Code
    627              
    628             any [qw/get post/] => '/multiple' => sub {
    629             my ( $login_form, $registration_form );
    630             debug to_dumper({params});
    631              
    632             if (params->{login}) {
    633             $login_form = form('logintest', source => 'parameters');
    634             my %vals = %{$login->values};
    635             # VALIDATE %vals here
    636             }
    637             else {
    638             # pick from session
    639             $login_form = form('logintest', source => 'session');
    640             }
    641              
    642             if (params->{register}) {
    643             $registration_form = form('registrationtest', source => 'parameters');
    644             my %vals = %{$registration->values};
    645             # VALIDATE %vals here
    646             }
    647             else {
    648             # pick from session
    649             $registration_form = form('registrationtest', source => 'session');
    650             }
    651             template multiple => { form => [ $login_form, $registration_form ] };
    652             };
    653              
    654             =head1 METHODS
    655              
    656             =head2 default_tmpl_ext
    657              
    658             Returns default template extension.
    659              
    660             =head2 render TEMPLATE TOKENS
    661              
    662             Renders template TEMPLATE with values from TOKENS.
    663              
    664             =head1 SEE ALSO
    665              
    666             L, L
    667              
    668             =head1 AUTHOR
    669              
    670             Author of the original Dancer module:
    671              
    672             Stefan Hornburg (Racke), C<< >>
    673              
    674             Conversion to Dancer2:
    675              
    676             Peter Mottram (SysPete), C<< >>
    677              
    678             Author of the original version of this Dancer2 module:
    679              
    680             William Carr (mrmaloof), C<< >>
    681              
    682             =head1 BUGS
    683              
    684             Please report any bugs or feature requests via the GitHub issue tracker at:
    685             L
    686              
    687             =head1 SUPPORT
    688              
    689             You can find documentation for this module with the perldoc command.
    690              
    691             perldoc Dancer2::Template::TemplateFlute
    692              
    693             You can also look for information at:
    694              
    695             =over 4
    696              
    697             =item * AnnoCPAN: Annotated CPAN documentation
    698              
    699             L
    700              
    701             =item * CPAN Ratings
    702              
    703             L
    704              
    705             =item * meta::cpan
    706              
    707             L
    708              
    709             =back
    710              
    711             =head1 LICENSE AND COPYRIGHT
    712              
    713             Copyright 2011-2016 Stefan Hornburg (Racke) .
    714              
    715             This program is free software; you can redistribute it and/or modify it
    716             under the terms of either: the GNU General Public License as published
    717             by the Free Software Foundation; or the Artistic License.
    718              
    719             See http://dev.perl.org/licenses/ for more information.
    720              
    721             =cut
    722              
    723             1;