package Mojolicious::Renderer; use Mojo::Base -base; use File::Spec::Functions 'catfile'; use Mojo::Cache; use Mojo::JSON; use Mojo::Home; use Mojo::Loader; use Mojo::Util qw(decamelize encode slurp); has cache => sub { Mojo::Cache->new }; has classes => sub { ['main'] }; has default_format => 'html'; has 'default_handler'; has encoding => 'UTF-8'; has handlers => sub { { data => sub { ${$_[2]} = $_[3]{data} }, text => sub { ${$_[2]} = $_[3]{text} }, json => sub { ${$_[2]} = Mojo::JSON->new->encode($_[3]{json}) } }; }; has helpers => sub { {} }; has paths => sub { [] }; # Bundled templates my $HOME = Mojo::Home->new; $HOME->parse( $HOME->parse($HOME->mojo_lib_dir)->rel_dir('Mojolicious/templates')); my %TEMPLATES = map { $_ => slurp $HOME->rel_file($_) } @{$HOME->list_files}; sub add_handler { shift->_add(handlers => @_) } sub add_helper { shift->_add(helpers => @_) } sub get_data_template { my ($self, $options) = @_; # Index DATA templates my $loader = Mojo::Loader->new; unless ($self->{index}) { my $index = $self->{index} = {}; for my $class (reverse @{$self->classes}) { $index->{$_} = $class for keys %{$loader->data($class)}; } } # Find template my $template = $self->template_name($options); return $loader->data($self->{index}{$template}, $template); } sub render { my ($self, $c, $args) = @_; $args ||= {}; # Localize "extends" and "layout" to allow argument overrides my $stash = $c->stash; local $stash->{layout} = $stash->{layout} if exists $stash->{layout}; local $stash->{extends} = $stash->{extends} if exists $stash->{extends}; delete @{$stash}{qw(layout extends)} if my $partial = $args->{partial}; # Merge stash and arguments @{$stash}{keys %$args} = values %$args; my $options = { encoding => $self->encoding, handler => $stash->{handler}, template => delete $stash->{template} }; my $inline = $options->{inline} = delete $stash->{inline}; $options->{handler} //= $self->default_handler if defined $inline; $options->{format} = $stash->{format} || $self->default_format; # Data my $output; if (defined(my $data = delete $stash->{data})) { $self->handlers->{data}->($self, $c, \$output, {data => $data}); return $output, $options->{format}; } # JSON elsif (my $json = delete $stash->{json}) { $self->handlers->{json}->($self, $c, \$output, {json => $json}); return $output, 'json'; } # Text elsif (defined(my $text = delete $stash->{text})) { $self->handlers->{text}->($self, $c, \$output, {text => $text}); } # Template or templateless handler else { $options->{template} ||= $self->_generate_template($c); return unless $self->_render_template($c, \$output, $options); } # Extends my $content = $stash->{'mojo.content'} ||= {}; local $content->{content} = $output if $stash->{extends} || $stash->{layout}; while ((my $extends = $self->_extends($stash)) && !defined $inline) { $options->{handler} = $stash->{handler}; $options->{format} = $stash->{format} || $self->default_format; $options->{template} = $extends; $self->_render_template($c, \$output, $options); $content->{content} = $output if $content->{content} !~ /\S/ && $output =~ /\S/; } # Encoding $output = encode $options->{encoding}, $output if !$partial && $options->{encoding} && $output; return $output, $options->{format}; } sub template_name { my ($self, $options) = @_; return undef unless my $template = $options->{template}; return undef unless my $format = $options->{format}; my $handler = $options->{handler}; return defined $handler ? "$template.$format.$handler" : "$template.$format"; } sub template_path { my $self = shift; # Nameless return undef unless my $name = $self->template_name(shift); # Search all paths for my $path (@{$self->paths}) { my $file = catfile($path, split '/', $name); return $file if -r $file; } # Fall back to first path return catfile($self->paths->[0], split '/', $name); } sub _add { my ($self, $attr, $name, $cb) = @_; $self->$attr->{$name} = $cb; return $self; } sub _bundled { $TEMPLATES{"@{[pop]}.html.ep"} } sub _detect_handler { my ($self, $options) = @_; # Templates return undef unless my $file = $self->template_name($options); unless ($self->{templates}) { s/\.(\w+)$// and $self->{templates}{$_} ||= $1 for map { sort @{Mojo::Home->new($_)->list_files} } @{$self->paths}; } return $self->{templates}{$file} if exists $self->{templates}{$file}; # DATA templates unless ($self->{data}) { my $loader = Mojo::Loader->new; my @templates = map { sort keys %{$loader->data($_)} } @{$self->classes}; s/\.(\w+)$// and $self->{data}{$_} ||= $1 for @templates; } return $self->{data}{$file} if exists $self->{data}{$file}; # Nothing return undef; } sub _extends { my ($self, $stash) = @_; my $layout = delete $stash->{layout}; $stash->{extends} ||= join('/', 'layouts', $layout) if $layout; return delete $stash->{extends}; } sub _generate_template { my ($self, $c) = @_; # Normal default template my $stash = $c->stash; my $controller = $stash->{controller}; my $action = $stash->{action}; return join '/', split(/-/, decamelize($controller)), $action if $controller && $action; # Try the route name if we don't have controller and action return undef unless my $endpoint = $c->match->endpoint; return $endpoint->name; } sub _render_template { my ($self, $c, $output, $options) = @_; # Find handler and render my $handler = $options->{handler} || $self->_detect_handler($options); $options->{handler} = $handler ||= $self->default_handler; if (my $renderer = $self->handlers->{$handler}) { return 1 if $renderer->($self, $c, $output, $options); } # No handler else { $c->app->log->error(qq{No handler for "$handler" available.}) } return undef; } 1; =encoding utf8 =head1 NAME Mojolicious::Renderer - Generate dynamic content =head1 SYNOPSIS use Mojolicious::Renderer; my $renderer = Mojolicious::Renderer->new; push @{$renderer->classes}, 'MyApp::Foo'; push @{renderer->paths}, '/home/sri/templates'; =head1 DESCRIPTION L is the standard L renderer. See L for more. =head1 ATTRIBUTES L implements the following attributes. =head2 cache my $cache = $renderer->cache; $renderer = $renderer->cache(Mojo::Cache->new); Renderer cache, defaults to a L object. =head2 classes my $classes = $renderer->classes; $renderer = $renderer->classes(['main']); Classes to use for finding templates in C sections, first one has the highest precedence, defaults to C
. # Add another class with templates in DATA section push @{$renderer->classes}, 'Mojolicious::Plugin::Fun'; =head2 default_format my $default = $renderer->default_format; $renderer = $renderer->default_format('html'); The default format to render if C is not set in the stash. =head2 default_handler my $default = $renderer->default_handler; $renderer = $renderer->default_handler('ep'); The default template handler to use for rendering in cases where auto detection doesn't work, like for C templates. =head2 encoding my $encoding = $renderer->encoding; $renderer = $renderer->encoding('koi8-r'); Will encode generated content if set, defaults to C. Note that many renderers such as L also use this value to determine if template files should be decoded before processing. =head2 handlers my $handlers = $renderer->handlers; $renderer = $renderer->handlers({epl => sub {...}}); Registered handlers, by default only C, C and C are already defined. =head2 helpers my $helpers = $renderer->helpers; $renderer = $renderer->helpers({url_for => sub {...}}); Registered helpers. =head2 paths my $paths = $renderer->paths; $renderer = $renderer->paths(['/home/sri/templates']); Directories to look for templates in, first one has the highest precedence. # Add another "templates" directory push @{$renderer->paths}, '/home/sri/templates'; =head1 METHODS L inherits all methods from L and implements the following new ones. =head2 add_handler $renderer = $renderer->add_handler(epl => sub {...}); Register a new handler. =head2 add_helper $renderer = $renderer->add_helper(url_for => sub {...}); Register a new helper. =head2 get_data_template my $template = $renderer->get_data_template({ template => 'foo/bar', format => 'html', handler => 'epl' }); Get a C section template by name, usually used by handlers. =head2 render my ($output, $format) = $renderer->render(Mojolicious::Controller->new); my ($output, $format) = $renderer->render(Mojolicious::Controller->new, { template => 'foo/bar', foo => 'bar' }); Render output through one of the renderers. See L for a more user-friendly interface. =head2 template_name my $template = $renderer->template_name({ template => 'foo/bar', format => 'html', handler => 'epl' }); Builds a template name based on an options hash reference with C