1package Dancer::Template::Abstract;
2our $AUTHORITY = 'cpan:SUKRIA';
3#ABSTRACT: abstract class for Dancer's template engines
4$Dancer::Template::Abstract::VERSION = '1.3513';
5use strict;
6use warnings;
7use Carp;
8
9use Dancer::Logger;
10use Dancer::Factory::Hook;
11use Dancer::FileUtils 'path';
12use Dancer::Exception qw(:all);
13
14use base 'Dancer::Engine';
15
16Dancer::Factory::Hook->instance->install_hooks(
17    qw/before_template_render after_template_render before_layout_render after_layout_render/
18);
19
20# overloads this method to implement the rendering
21# args:   $self, $template, $tokens
22# return: a string of $template's content processed with $tokens
23sub render { confess "render not implemented" }
24
25sub default_tmpl_ext { "tt" }
26
27# Work out the template names to look for; this will be the view name passed
28# as-is, and also the view name with the default template extension appended, if
29# it did not already end in that.
30sub _template_name {
31    my ( $self, $view ) = @_;
32    my @views = ( $view );
33    my $def_tmpl_ext = $self->config->{extension} || $self->default_tmpl_ext();
34    push @views, $view .= ".$def_tmpl_ext" if $view !~ /\.\Q$def_tmpl_ext\E$/;
35    return @views;
36}
37
38sub view {
39    my ($self, $view) = @_;
40
41    my $views_dir = Dancer::App->current->setting('views');
42
43    for my $template ($self->_template_name($view)) {
44        my $view_path = path($views_dir, $template);
45        return $view_path if -f $view_path;
46    }
47
48    # No matching view path was found
49    return;
50}
51
52sub layout {
53    my ($self, $layout, $tokens, $content) = @_;
54
55    my $layouts_dir = path(Dancer::App->current->setting('views'), 'layouts');
56    my $layout_path;
57    for my $layout_name ($self->_template_name($layout)) {
58        $layout_path = path($layouts_dir, $layout_name);
59        last if -e $layout_path;
60    }
61
62    my $full_content;
63    if (-e $layout_path) {
64        $full_content = Dancer::Template->engine->render(
65                                     $layout_path, {%$tokens, content => $content});
66    } else {
67        $full_content = $content;
68        Dancer::Logger::error("Defined layout ($layout) was not found!");
69    }
70    $full_content;
71}
72
73sub apply_renderer {
74    my ($self, $view, $tokens) = @_;
75
76    ($tokens, undef) = _prepare_tokens_options($tokens);
77
78    $view = $self->view($view);
79
80    Dancer::Factory::Hook->execute_hooks('before_template_render', $tokens);
81
82    my $content;
83    try {
84        $content = $self->render($view, $tokens);
85    } continuation {
86        my ($continuation) = @_;
87        # If we have a Route continuation, run the after hook, then
88        # propagate the continuation
89        Dancer::Factory::Hook->execute_hooks('after_template_render', \$content);
90        $continuation->rethrow();
91    };
92
93    Dancer::Factory::Hook->execute_hooks('after_template_render', \$content);
94
95    # make sure to avoid ( undef ) in list context return
96    defined $content
97      and return $content;
98    return;
99}
100
101sub apply_layout {
102    my ($self, $content, $tokens, $options) = @_;
103
104    ($tokens, $options) = _prepare_tokens_options($tokens, $options);
105
106    # If 'layout' was given in the options hashref, use it if it's a true value,
107    # or don't use a layout if it was false (0, or undef); if layout wasn't
108    # given in the options hashref, go with whatever the current layout setting
109    # is.
110    my $layout =
111      exists $options->{layout}
112      ? ($options->{layout} ? $options->{layout} : undef)
113      : Dancer::App->current->setting('layout');
114
115    defined $content or return;
116
117    defined $layout or return $content;
118
119    Dancer::Factory::Hook->execute_hooks('before_layout_render', $tokens, \$content);
120
121    my $full_content;
122
123    try {
124        $full_content = $self->layout($layout, $tokens, $content);
125    } continuation {
126        my ($continuation) = @_;
127        # If we have a Route continuation, run the after hook, then
128        # propagate the continuation
129        Dancer::Factory::Hook->execute_hooks('after_layout_render', \$full_content);
130        $continuation->rethrow();
131    };
132
133    Dancer::Factory::Hook->execute_hooks('after_layout_render', \$full_content);
134
135    # make sure to avoid ( undef ) in list context return
136    defined $full_content
137      and return $full_content;
138    return;
139}
140
141sub _prepare_tokens_options {
142    my ($tokens, $options) = @_;
143
144    $options ||= {};
145
146    # these are the default tokens provided for template processing
147    $tokens ||= {};
148    $tokens->{perl_version}   = $];
149    $tokens->{dancer_version} = $Dancer::VERSION;
150    $tokens->{settings}       = Dancer::Config->settings;
151
152    # If we're processing a request, also add the request object, params and
153    # vars as tokens:
154    if (my $request = Dancer::SharedData->request) {
155        $tokens->{request}        = $request;
156        $tokens->{params}         = $request->params;
157        $tokens->{vars}           = Dancer::SharedData->vars;
158    }
159
160    Dancer::App->current->setting('session')
161      and $tokens->{session} = Dancer::Session->get;
162
163    return ($tokens, $options);
164}
165
166sub template {
167    my ($class, $view, $tokens, $options) = @_;
168    my ($content, $full_content);
169
170    my $engine = Dancer::Template->engine;
171
172    # it's important that $tokens is not undef, so that things added to it via
173    # a before_template in apply_renderer survive to the apply_layout. GH#354
174    $tokens  ||= {};
175    $options ||= {};
176
177    if ($view) {
178        # check if the requested view exists
179        my $view_path = $engine->view($view) || '';
180        if ($engine->view_exists($view_path)) {
181            $content = $engine->apply_renderer($view, $tokens);
182        } else {
183            Dancer::Logger::error(
184                "Supplied view ($view) not found - $view_path does not exist"
185            );
186            return Dancer::Error->new(
187                          code => 500,
188                          message => 'view not found',
189                   )->render();
190        }
191    } else {
192        $content = delete $options->{content};
193    }
194
195    defined $content and $full_content =
196      $engine->apply_layout($content, $tokens, $options);
197
198    defined $full_content
199      and return $full_content;
200
201    Dancer::Error->new(
202        code    => 404,
203        message => "Page not found",
204    )->render();
205}
206
207sub view_exists { return defined $_[1] &&  -f $_[1] }
208
2091;
210
211__END__
212
213=pod
214
215=encoding UTF-8
216
217=head1 NAME
218
219Dancer::Template::Abstract - abstract class for Dancer's template engines
220
221=head1 VERSION
222
223version 1.3513
224
225=head1 DESCRIPTION
226
227This class is provided as a base class for each template engine. Any template
228engine must inherit from it and provide a set of methods described below.
229
230=head1 TEMPLATE TOKENS
231
232By default Dancer injects some tokens (or variables) to templates. The
233available tokens are:
234
235=over 4
236
237=item C<perl_version>
238
239The current running Perl version.
240
241=item C<dancer_version>
242
243The current running Dancer version.
244
245=item C<settings>
246
247Hash to access current application settings.
248
249=item C<request>
250
251Hash to access your current request.
252
253=item C<params>
254
255Hash to access your request parameters.
256
257=item C<vars>
258
259Hash to access your defined variables (using C<vars>).
260
261=item C<session>
262
263Hash to access your session (if you have session enabled)
264
265=back
266
267=head1 INTERFACE
268
269=over 4
270
271=item B<init()>
272
273The template engine can overload this method if some initialization stuff has to
274be done before the template engine is used.
275
276The base class provides a plain init() method that only returns true.
277
278=item B<default_tmpl_ext()>
279
280Template class that inherits this class should override this method to return a default template
281extension, example: for Template::Toolkit it returns "tt" and for HTML::Mason it returns "mason".
282So when you call C<template 'index';> in your dispatch code, Dancer will look for a file 'index.tt'
283or 'index.mason' based on the template you use.
284
285Note 1: when returning the extension string, please do not add a dot in front of the extension
286as Dancer will do that.
287
288Note 2: for backwards compatibility abstract class returns "tt" instead of throwing
289an exception 'method not implemented'.
290
291User would be able to change the default extension using the
292C<<extension>> configuration variable on the template
293configuration. For example, for the default (C<Simple>) engine:
294
295     template: "simple"
296     engines:
297       simple:
298         extension: 'tmpl'
299
300=item B<view($view)>
301
302The default behavior of this method is to return the path of the given view,
303appending the default template extension (either the value of the C<extension>
304setting in the configuration, or the value returned by C<default_tmpl_ext>) if
305it is not present in the view name given and no layout template with that exact
306name existed.  (In other words, given a layout name C<main>, if C<main> exists
307in the layouts dir, it will be used; if not, C<main.tmpl> (where C<tmpl> is the
308value of the C<extension> setting, or the value returned by C<default_tmpl_ext>)
309will be looked for.)
310
311=item B<view_exists($view_path)>
312
313By default, Dancer::Template::Abstract checks to see if it can find the
314view file calling C<view_exists($path_to_file)>. If not, it will
315generate a nice error message for the user.
316
317If you are using extending Dancer::Template::Abstract to use a template
318system with multiple document roots (like L<Text::XSlate> or
319L<Template>), you can override this method to always return true, and
320therefore skip this check.
321
322=item B<layout($layout, $tokens, $content)>
323
324The default behavior of this method is to merge a content with a layout.  The
325layout file is looked for with similar logic as per C<view> - an exact match
326first, then attempting to append the default template extension, if the view
327name given did not already end with it.
328
329=item B<render($self, $template, $tokens)>
330
331This method must be implemented by the template engine. Given a template and a
332set of tokens, it returns a processed string.
333
334If C<$template> is a reference, it's assumed to be a reference to a string that
335contains the template itself. If it's not a reference, it's assumed to be the
336path to template file, as a string. The render method will then have to open it
337and read its content (Dancer::FileUtils::read_file_content does that job).
338
339This method's return value must be a string which is the result of the
340interpolation of C<$tokens> in C<$template>.
341
342If an error occurs, the method should trigger an exception with C<die()>.
343
344Examples :
345
346    # with a template as a file
347    $content = $engine->render('/my/template.txt', { var => 42 };
348
349    # with a template as a scalar
350    my $template = "here is <% var %>";
351    $content = $engine->render(\$template, { var => 42 });
352
353=back
354
355=head1 AUTHOR
356
357This module has been written by Alexis Sukrieh, see L<Dancer> for details.
358
359=head1 AUTHOR
360
361Dancer Core Developers
362
363=head1 COPYRIGHT AND LICENSE
364
365This software is copyright (c) 2010 by Alexis Sukrieh.
366
367This is free software; you can redistribute it and/or modify it under
368the same terms as the Perl 5 programming language system itself.
369
370=cut
371