# ABSTRACT: Role for template engines package Dancer2::Core::Role::Template; $Dancer2::Core::Role::Template::VERSION = '1.1.0'; use Dancer2::Core::Types; use Dancer2::FileUtils 'path'; use Carp 'croak'; use Ref::Util qw< is_ref >; use Moo::Role; with 'Dancer2::Core::Role::Engine'; sub hook_aliases { { before_template_render => 'engine.template.before_render', after_template_render => 'engine.template.after_render', before_layout_render => 'engine.template.before_layout_render', after_layout_render => 'engine.template.after_layout_render', } } sub supported_hooks { values %{ shift->hook_aliases } } sub _build_type {'Template'} requires 'render'; has log_cb => ( is => 'ro', isa => CodeRef, default => sub { sub {1} }, ); has name => ( is => 'ro', lazy => 1, builder => 1, ); sub _build_name { ( my $name = ref shift ) =~ s/^Dancer2::Template:://; $name; } has charset => ( is => 'ro', isa => Str, default => sub {'UTF-8'}, ); has default_tmpl_ext => ( is => 'ro', isa => Str, default => sub { shift->config->{extension} || 'tt' }, ); has engine => ( is => 'ro', isa => Object, lazy => 1, builder => 1, ); has settings => ( is => 'ro', isa => HashRef, lazy => 1, default => sub { +{} }, writer => 'set_settings', ); # The attributes views, layout and layout_dir have triggers in # Dancer2::Core::App that enable their values to be modified by # the `set` keyword. As such, these are defined as read-write attrs. has views => ( is => 'rw', isa => Maybe [Str], ); has layout => ( is => 'rw', isa => Maybe [Str], ); has layout_dir => ( is => 'rw', isa => Maybe [Str], ); sub _template_name { my ( $self, $view ) = @_; my $def_tmpl_ext = $self->default_tmpl_ext(); $view .= ".$def_tmpl_ext" if $view !~ /\.\Q$def_tmpl_ext\E$/; return $view; } sub view_pathname { my ( $self, $view ) = @_; $view = $self->_template_name($view); return path( $self->views, $view ); } sub layout_pathname { my ( $self, $layout ) = @_; return path( $self->views, $self->layout_dir, $self->_template_name($layout), ); } sub pathname_exists { my ( $self, $pathname ) = @_; return -f $pathname; } sub render_layout { my ( $self, $layout, $tokens, $content ) = @_; $layout = $self->layout_pathname($layout); # FIXME: not sure if I can "just call render" $self->render( $layout, { %$tokens, content => $content } ); } sub apply_renderer { my ( $self, $view, $tokens ) = @_; $view = $self->view_pathname($view) if !is_ref($view); $tokens = $self->_prepare_tokens_options( $tokens ); $self->execute_hook( 'engine.template.before_render', $tokens ); my $content = $self->render( $view, $tokens ); $self->execute_hook( 'engine.template.after_render', \$content ); # make sure to avoid ( undef ) in list context return defined $content and return $content; return; } sub apply_layout { my ( $self, $content, $tokens, $options ) = @_; $tokens = $self->_prepare_tokens_options( $tokens ); # If 'layout' was given in the options hashref, use it if it's a true value, # or don't use a layout if it was false (0, or undef); if layout wasn't # given in the options hashref, go with whatever the current layout setting # is. my $layout = exists $options->{layout} ? ( $options->{layout} ? $options->{layout} : undef ) : ( $self->layout || $self->config->{layout} ); # that should only be $self->config, but the layout ain't there ??? defined $content or return; defined $layout or return $content; $self->execute_hook( 'engine.template.before_layout_render', $tokens, \$content ); my $full_content = $self->render_layout( $layout, $tokens, $content ); $self->execute_hook( 'engine.template.after_layout_render', \$full_content ); # make sure to avoid ( undef ) in list context return defined $full_content and return $full_content; return; } sub _prepare_tokens_options { my ( $self, $tokens ) = @_; # these are the default tokens provided for template processing $tokens ||= {}; $tokens->{perl_version} = $^V; $tokens->{dancer_version} = Dancer2->VERSION; $tokens->{settings} = $self->settings; # no request when template is called as a global keyword if ( $self->has_request ) { $tokens->{request} = $self->request; $tokens->{params} = $self->request->params; $tokens->{vars} = $self->request->vars; # a session can not exist if there is no request $tokens->{session} = $self->session->data if $self->has_session; } return $tokens; } sub process { my ( $self, $view, $tokens, $options ) = @_; my ( $content, $full_content ); # it's important that $tokens is not undef, so that things added to it via # a before_template in apply_renderer survive to the apply_layout. GH#354 $tokens ||= {}; $options ||= {}; ## FIXME - Look into PR 654 so we fix the problem here as well! $content = $view ? $self->apply_renderer( $view, $tokens ) : delete $options->{content}; defined $content and $full_content = $self->apply_layout( $content, $tokens, $options ); defined $full_content and return $full_content; croak "Template did not produce any content"; } 1; __END__ =pod =encoding UTF-8 =head1 NAME Dancer2::Core::Role::Template - Role for template engines =head1 VERSION version 1.1.0 =head1 DESCRIPTION Any class that consumes this role will be able to be used as a template engine under Dancer2. In order to implement this role, the consumer B implement the method C. This method will receive three arguments: =over 4 =item $self =item $template =item $tokens =back Any template receives the following tokens, by default: =over 4 =item * C Current version of perl, effectively C<$^V>. =item * C Current version of Dancer2, effectively C<< Dancer2->VERSION >>. =item * C A hash of the application configuration. =item * C The current request object. =item * C A hash reference of all the parameters. Currently the equivalent of C<< $request->params >>. =item * C The list of request variables, which is what you would get if you called the C keyword. =item * C The current session data, if a session exists. =back =head1 ATTRIBUTES =head2 name The name of the template engine (e.g.: Simple). =head2 charset The charset. The default value is B. =head2 default_tmpl_ext The default file extension. If not provided, B is used. =head2 views Path to the directory containing the views. =head2 layout Path to the directory containing the layouts. =head2 layout_dir Relative path to the layout directory. Default: B. =head2 engine Contains the engine. =head1 METHODS =head2 view_pathname($view) Returns the full path to the requested view. =head2 layout_pathname($layout) Returns the full path to the requested layout. =head2 pathname_exists($pathname) Returns true if the requested pathname exists. Can be used for either views or layouts: $self->pathname_exists( $self->view_pathname( 'some_view' ) ); $self->pathname_exists( $self->layout_pathname( 'some_layout' ) ); =head2 render_layout($layout, \%tokens, \$content) Render the layout with the applied tokens =head2 apply_renderer($view, \%tokens) =head2 apply_layout($content, \%tokens, \%options) =head2 process($view, \%tokens, \%options) =head2 template($view, \%tokens, \%options) =head1 AUTHOR Dancer Core Developers =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2023 by Alexis Sukrieh. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut