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

    Register

    586            
    587            
    588            
    Info
    589            
    590            
  • 591            
    592            
    593            
    594            
  • 595            
    596            
    597            
    598            
  • 599            
    600            
    601            
    602            
  • 603            
    604            
    605            
    606            
    607            
    608            

    Login

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