package Trog::Renderer; use 5.040; use re '/aasx'; use warnings FATAL => 'all'; use Carp::Always; use Trog::Vars; use Trog::Log qw{:all}; use Trog::Renderer::text; use Trog::Renderer::html; use Trog::Renderer::json; use Trog::Renderer::blob; use Trog::Renderer::css; use Trog::Renderer::email; =head1 Trog::Renderer Idea here is to have a renderer per known/supported content-type we need to output that is also theme-aware. We have an abstraction here, render() which you feed everything to. =cut; our %renderers = ( text => \&Trog::Renderer::text::render, html => \&Trog::Renderer::html::render, json => \&Trog::Renderer::json::render, blob => \&Trog::Renderer::blob::render, xsl => \&Trog::Renderer::text::render, xml => \&Trog::Renderer::text::render, rss => \&Trog::Renderer::html::render, css => \&Trog::Renderer::css::render, email => \&Trog::Renderer::email::render, ); =head2 Trog::Renderer->render(%options) Returns either the 3-arg arrayref suitable to emit at the end of a PSGI session or a response body if the component field of options is truthy. The idea is that components will be concatenated to other rendered templates until we finish having everything ready. =cut sub render ( $class, %options ) { local $@; my $renderer; return _yeet( $renderer, "Renderer requires a valid content type to be passed", %options ) unless $options{contenttype}; my $rendertype = $Trog::Vars::byct{ $options{contenttype} }; return _yeet( $renderer, "Renderer requires a known content type (used $options{contenttype}) to be passed", %options ) unless $rendertype; $renderer = $renderers{$rendertype}; return _yeet( $renderer, "Renderer for $rendertype is not defined!", %options ) unless $renderer; return _yeet( $renderer, "Status code not provided", %options ) if !$options{code} && !$options{component}; return _yeet( $renderer, "Template data not provided", %options ) unless $options{data}; return _yeet( $renderer, "Template not provided", %options ) unless $options{template}; #TODO future - save the components too and then compose them? my $data = $options{data}; # $data->{nocache} as well as $options{nocache}: TCMS::build_routes puts a # route's nocache flag on the query, and exactly one caller (login) has ever # forwarded it here as an option. Every other nocache route was relying on # being auth-gated, so that the $data->{user} test below caught it -- which # says nothing about a route that is nocache and public, and there is now # one: a datasource whose posts go stale asks for it this way. my $skip_save = $options{component} || $options{nocache} || $data->{nocache} || !$data->{route} || $data->{has_query} || $data->{user} || ( $options{code} // 0 ) != 200 || Trog::Log::is_debug(); my $ret; local $@; eval { $ret = $renderer->(%options); if ( exists $data->{tpsgi} && !$skip_save ) { $data->{tpsgi}->add_post_close_callback( sub { # We invalidate these in the individual routes themselves. $data->{tpsgi}->save_render( $data->{route}, $rendertype, $ret->[2][0] ); } ); } 1; } or do { return _yeet( $renderer, $@, %options ); }; return $ret; } sub _yeet ( $renderer, $error, %options ) { WARN($error); # All-else fails error page my $ret; local $@; eval { $ret = $renderer->( code => 500, template => '500.tx', contenttype => 'text/html', data => { %options, content => "

500 Internal Server Error

$error" }, ); 1; } or do { my $msg = $error; $msg .= " and subsequently during render of error template, $@" if $renderer; #XXX bytes is probably not correct here INFO( "$options{data}{method} 500 " . length($msg) . " $options{data}{route}" ); FATAL($msg); }; return $ret; } 1;