| File: | lib/Yukki/Web/View.pm |
| Coverage: | 83.9% |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | package Yukki::Web::View; | ||||||
| 2 | |||||||
| 3 | 7 7 | 51 20 | use v5.24; | ||||
| 4 | 7 7 7 | 21 7 35 | use utf8; | ||||
| 5 | 7 7 7 | 96 26 20 | use Moo; | ||||
| 6 | |||||||
| 7 | 7 7 7 | 3566 116465 40 | use Type::Params qw( validate ); | ||||
| 8 | 7 7 7 | 1105 8 217 | use Scalar::Util qw( blessed reftype ); | ||||
| 9 | 7 7 7 | 2231 108696 128 | use Spreadsheet::Engine; | ||||
| 10 | 7 7 7 | 5172 191105 150 | use Template::Pure; | ||||
| 11 | 7 7 7 | 3280 135663 208 | use Text::MultiMarkdown; | ||||
| 12 | 7 7 7 | 219 577 184 | use Try::Tiny; | ||||
| 13 | 7 7 7 | 464 4338 45 | use Type::Utils; | ||||
| 14 | 7 7 7 | 6385 9 31 | use Types::Standard qw( Dict Str ArrayRef HashRef slurpy ); | ||||
| 15 | |||||||
| 16 | 7 7 7 | 4580 4464 40 | use namespace::clean; | ||||
| 17 | |||||||
| 18 | # ABSTRACT: base class for Yukki::Web views | ||||||
| 19 | |||||||
| 20 - 30 | =head1 DESCRIPTION This is the base class for all L<Yukki::Web> views. =head1 ATTRIBUTES =head2 app This is the L<Yukki::Web> singleton. =cut | ||||||
| 31 | |||||||
| 32 | has app => ( | ||||||
| 33 | is => 'ro', | ||||||
| 34 | isa => class_type('Yukki::Web'), | ||||||
| 35 | required => 1, | ||||||
| 36 | weak_ref => 1, | ||||||
| 37 | handles => 'Yukki::Role::App', | ||||||
| 38 | ); | ||||||
| 39 | |||||||
| 40 - 47 | =head2 markdown This is the L<Text::MultiMarkdown> object for rendering L</yukkitext>. Do not use. Provides a C<format_markdown> method delegated to C<markdown>. Do not use. =cut | ||||||
| 48 | |||||||
| 49 | has markdown => ( | ||||||
| 50 | is => 'ro', | ||||||
| 51 | isa => class_type('Text::MultiMarkdown'), | ||||||
| 52 | required => 1, | ||||||
| 53 | lazy => 1, | ||||||
| 54 | builder => '_build_markdown', | ||||||
| 55 | handles => { | ||||||
| 56 | 'format_markdown' => 'markdown', | ||||||
| 57 | }, | ||||||
| 58 | ); | ||||||
| 59 | |||||||
| 60 | sub _build_markdown { | ||||||
| 61 | 0 | 0 | Text::MultiMarkdown->new( | ||||
| 62 | markdown_in_html_blocks => 1, | ||||||
| 63 | heading_ids => 0, | ||||||
| 64 | ); | ||||||
| 65 | } | ||||||
| 66 | |||||||
| 67 - 71 | =head2 messages_template This is the template used to render info, warning, and error messages to the page. =cut | ||||||
| 72 | |||||||
| 73 | has messages_template => ( | ||||||
| 74 | is => 'ro', | ||||||
| 75 | isa => class_type('Template::Pure'), | ||||||
| 76 | lazy => 1, | ||||||
| 77 | builder => '_build_messages_template', | ||||||
| 78 | ); | ||||||
| 79 | |||||||
| 80 | sub _build_messages_template { | ||||||
| 81 | 2 | 24 | my $self = shift; | ||||
| 82 | 2 | 19 | return $self->prepare_template( | ||||
| 83 | template => 'messages.html', | ||||||
| 84 | directives => [ | ||||||
| 85 | '.error' => { | ||||||
| 86 | 'error<-errors' => [ | ||||||
| 87 | '.' => 'error', | ||||||
| 88 | ], | ||||||
| 89 | }, | ||||||
| 90 | '.warning' => { | ||||||
| 91 | 'warning<-warnings' => [ | ||||||
| 92 | '.' => 'warning', | ||||||
| 93 | ], | ||||||
| 94 | }, | ||||||
| 95 | '.info' => { | ||||||
| 96 | 'one_info<-info' => [ | ||||||
| 97 | '.' => 'one_info', | ||||||
| 98 | ], | ||||||
| 99 | }, | ||||||
| 100 | ], | ||||||
| 101 | ); | ||||||
| 102 | } | ||||||
| 103 | |||||||
| 104 | has _page_templates => ( | ||||||
| 105 | is => 'ro', | ||||||
| 106 | isa => HashRef, | ||||||
| 107 | required => 1, | ||||||
| 108 | default => sub { +{} }, | ||||||
| 109 | ); | ||||||
| 110 | |||||||
| 111 - 115 | =head2 links_template This is the template object used to render links. =cut | ||||||
| 116 | |||||||
| 117 | has links_template => ( | ||||||
| 118 | is => 'ro', | ||||||
| 119 | isa => class_type('Template::Pure'), | ||||||
| 120 | lazy => 1, | ||||||
| 121 | builder => '_build_links_template', | ||||||
| 122 | ); | ||||||
| 123 | |||||||
| 124 | sub _build_links_template { | ||||||
| 125 | 0 | 0 | my $self = shift; | ||||
| 126 | 0 | 0 | $self->prepare_template( | ||||
| 127 | template => 'links.html', | ||||||
| 128 | directives => [ | ||||||
| 129 | '.links' => { | ||||||
| 130 | 'link<-links' => [ | ||||||
| 131 | 'a' => 'link.label', | ||||||
| 132 | 'a@href' => 'link.href', | ||||||
| 133 | ], | ||||||
| 134 | }, | ||||||
| 135 | ], | ||||||
| 136 | ); | ||||||
| 137 | } | ||||||
| 138 | |||||||
| 139 - 147 | =head1 METHODS
=head2 page_template
my $template = $self->page_template('default');
Returns the template used to render pages for the given style name.
=cut | ||||||
| 148 | |||||||
| 149 | sub page_template { | ||||||
| 150 | 2 | 1 | 6 | my ($self, $which) = @_; | |||
| 151 | |||||||
| 152 | return $self->_page_templates->{ $which } | ||||||
| 153 | 2 | 11 | if $self->_page_templates->{ $which }; | ||||
| 154 | |||||||
| 155 | 2 | 7 | my $view = $which // 'default'; | ||||
| 156 | 2 | 44 | my $view_args = $self->app->settings->page_views->{ $view } | ||||
| 157 | // { template => 'shell.html' }; | ||||||
| 158 | 2 | 28 | $view_args->{directives} //= []; | ||||
| 159 | |||||||
| 160 | my %menu_vars = map { | ||||||
| 161 | 8 | 27 | my $menu_name = $_; | ||||
| 162 | 8 | 48 | "#nav-$menu_name .navigation" => { | ||||
| 163 | "menu_item<-$menu_name" => [ | ||||||
| 164 | 'a' => 'menu_item.label', | ||||||
| 165 | 'a@href' => 'menu_item.href', | ||||||
| 166 | ], | ||||||
| 167 | }, | ||||||
| 168 | 2 2 | 5 41 | } @{ $self->app->settings->menu_names }; | ||||
| 169 | |||||||
| 170 | return $self->_page_templates->{ $which } = $self->prepare_template( | ||||||
| 171 | template => $view_args->{template}, | ||||||
| 172 | directives => [ | ||||||
| 173 | $view_args->{directives}->@*, | ||||||
| 174 | 2 | 28 | 'head script.local' => { | ||||
| 175 | 'script<-scripts' => [ | ||||||
| 176 | '@src' => 'script', | ||||||
| 177 | ], | ||||||
| 178 | }, | ||||||
| 179 | 'head link.local' => { | ||||||
| 180 | 'link<-links' => [ | ||||||
| 181 | '@href' => 'link', | ||||||
| 182 | ], | ||||||
| 183 | }, | ||||||
| 184 | '#messages' => 'messages | encoded_string', | ||||||
| 185 | 'title' => 'main_title', | ||||||
| 186 | '.masthead-title' => 'title', | ||||||
| 187 | %menu_vars, | ||||||
| 188 | '#breadcrumb li' => { | ||||||
| 189 | 'crumb<-breadcrumb' => [ | ||||||
| 190 | 'a' => 'crumb.label', | ||||||
| 191 | 'a@href' => 'crumb.href', | ||||||
| 192 | ], | ||||||
| 193 | }, | ||||||
| 194 | '#content' => 'content | encoded_string', | ||||||
| 195 | ], | ||||||
| 196 | ); | ||||||
| 197 | } | ||||||
| 198 | |||||||
| 199 - 212 | =head2 prepare_template
my $template = $self->prepare_template({
template => 'foo.html',
directives => { ... },
});
This prepares a template for later rendering.
The C<template> is the name of the template file to use.
The C<directives> are the L<Template::Pure> directives to apply data given at render time to modify the template to create the output.
=cut | ||||||
| 213 | |||||||
| 214 | sub prepare_template { | ||||||
| 215 | 6 | 1 | 28 | my ($self, $opt) | |||
| 216 | = validate(\@_, class_type(__PACKAGE__), | ||||||
| 217 | slurpy Dict[ | ||||||
| 218 | template => Str, | ||||||
| 219 | directives => ArrayRef, | ||||||
| 220 | ]); | ||||||
| 221 | 6 6 | 29441 422 | my ($template, $directives) = @{$opt}{qw( template directives )}; | ||||
| 222 | |||||||
| 223 | 6 | 31 | my $template_content = | ||||
| 224 | $self->app->locate_dir('template_path', $template)->slurp; | ||||||
| 225 | |||||||
| 226 | 6 | 853 | return Template::Pure->new( | ||||
| 227 | template => $template_content, | ||||||
| 228 | directives => $directives, | ||||||
| 229 | ); | ||||||
| 230 | } | ||||||
| 231 | |||||||
| 232 - 247 | =head2 render_page
my $document = $self->render_page({
template => 'foo.html',
context => $ctx,
vars => { ... },
});
This renders the given template and places it into the content section of the
F<shell.html> template.
The C<context> is used to render parts of the shell template.
The C<vars> are processed against the given template with L<Template::Pure>.
=cut | ||||||
| 248 | |||||||
| 249 | sub render_page { | ||||||
| 250 | 2 | 1 | 946 | my ($self, $opt) | |||
| 251 | = validate(\@_, class_type(__PACKAGE__), | ||||||
| 252 | slurpy Dict[ | ||||||
| 253 | template => class_type('Template::Pure'), | ||||||
| 254 | context => class_type('Yukki::Web::Context'), | ||||||
| 255 | vars => HashRef, | ||||||
| 256 | ]); | ||||||
| 257 | 2 2 | 13842 180 | my ($template, $ctx, $vars) = @{$opt}{qw( template context vars )}; | ||||
| 258 | 2 | 9 | $vars //= {}; | ||||
| 259 | |||||||
| 260 | 2 | 46 | my $messages = $self->render( | ||||
| 261 | template => $self->messages_template, | ||||||
| 262 | context => $ctx, | ||||||
| 263 | vars => { | ||||||
| 264 | errors => $ctx->has_errors ? [ $ctx->list_errors ] : undef, | ||||||
| 265 | warnings => $ctx->has_warnings ? [ $ctx->list_warnings ] : undef, | ||||||
| 266 | info => $ctx->has_info ? [ $ctx->list_info ] : undef, | ||||||
| 267 | }, | ||||||
| 268 | ); | ||||||
| 269 | |||||||
| 270 | 2 | 4133 | my ($main_title, $title); | ||||
| 271 | 2 | 61 | if ($ctx->response->has_page_title) { | ||||
| 272 | 2 | 60 | $title = $ctx->response->page_title; | ||||
| 273 | 2 | 90 | $main_title = $ctx->response->page_title . ' - Yukki'; | ||||
| 274 | } | ||||||
| 275 | else { | ||||||
| 276 | 0 | 0 | $title = $main_title = 'Yukki'; | ||||
| 277 | } | ||||||
| 278 | |||||||
| 279 | my %menu_vars = map { | ||||||
| 280 | 8 | 42 | $_ => $self->available_menu_items($ctx, $_) | ||||
| 281 | 2 2 | 50 41 | } @{ $self->app->settings->menu_names }; | ||||
| 282 | |||||||
| 283 | 2 | 43 | my @scripts = $self->app->settings->all_scripts; | ||||
| 284 | 2 | 39 | my @styles = $self->app->settings->all_styles; | ||||
| 285 | |||||||
| 286 | 2 | 39 | my $view = $ctx->request->parameters->{view} // 'default'; | ||||
| 287 | |||||||
| 288 | 2 | 592 | $vars->{'head script.local'} //= []; | ||||
| 289 | 2 | 23 | $vars->{'head link.local'} //= []; | ||||
| 290 | |||||||
| 291 | return $self->render( | ||||||
| 292 | template => $self->page_template($view), | ||||||
| 293 | context => $ctx, | ||||||
| 294 | vars => { | ||||||
| 295 | $vars->%*, | ||||||
| 296 | scripts => [ | ||||||
| 297 | 12 | 20032 | map { $ctx->rebase_url($_) } | ||||
| 298 | @scripts, | ||||||
| 299 | $vars->{'head script.local'}->@*, | ||||||
| 300 | ], | ||||||
| 301 | links => [ | ||||||
| 302 | 4 | 668 | map { $ctx->rebase_url($_) } | ||||
| 303 | @styles, | ||||||
| 304 | $vars->{'head link.local'}->@*, | ||||||
| 305 | ], | ||||||
| 306 | 'messages' => $messages, | ||||||
| 307 | 'main_title' => $main_title, | ||||||
| 308 | 'title' => $title, | ||||||
| 309 | %menu_vars, | ||||||
| 310 | 'breadcrumb' => $ctx->response->has_breadcrumb ? [ | ||||||
| 311 | map { | ||||||
| 312 | 2 | 12 | +{ | ||||
| 313 | %$_, | ||||||
| 314 | 1 | 9 | href => $ctx->rebase_url($_->{href}), | ||||
| 315 | } | ||||||
| 316 | } $ctx->response->breadcrumb_links | ||||||
| 317 | ] : undef, | ||||||
| 318 | 'content' => $self->render( | ||||||
| 319 | template => $template, | ||||||
| 320 | context => $ctx, | ||||||
| 321 | vars => $vars, | ||||||
| 322 | ), | ||||||
| 323 | }, | ||||||
| 324 | ); | ||||||
| 325 | } | ||||||
| 326 | |||||||
| 327 - 333 | =head2 available_menu_items my @items = $self->available_menu_items($ctx, 'menu_name'); Retrieves the navigation menu from the L<Yukki::Web::Response> and purges any links that the current user does not have access to. =cut | ||||||
| 334 | |||||||
| 335 | sub available_menu_items { | ||||||
| 336 | 8 | 1 | 20 | my ($self, $ctx, $name) = @_; | |||
| 337 | |||||||
| 338 | my @items = map { | ||||||
| 339 | +{ | ||||||
| 340 | %$_, | ||||||
| 341 | 9 | 1297 | href => $ctx->rebase_url($_->{href}), | ||||
| 342 | }, | ||||||
| 343 | } grep { | ||||||
| 344 | 8 15 15 | 141 33 31 | my $url = $_->{href}; $url =~ s{\?.*$}{}; | ||||
| 345 | |||||||
| 346 | 15 | 207 | my $match = $self->app->router->match($url); | ||||
| 347 | 15 | 99 | return unless $match; | ||||
| 348 | 15 | 35 | my $access_level_needed = $match->access_level; | ||||
| 349 | $self->check_access( | ||||||
| 350 | user => $ctx->session->{user}, | ||||||
| 351 | repository => $match->mapping->{repository} // '-', | ||||||
| 352 | 15 | 227 | special => $match->mapping->{special} // '-', | ||||
| 353 | needs => $access_level_needed, | ||||||
| 354 | ); | ||||||
| 355 | } $ctx->response->navigation_menu($name); | ||||||
| 356 | |||||||
| 357 | 8 | 764 | return @items ? \@items : undef; | ||||
| 358 | } | ||||||
| 359 | |||||||
| 360 - 366 | =head2 render_links my $document = $self->render_links($ctx, \@navigation_links); This renders a set of links using the F<links.html> template. =cut | ||||||
| 367 | |||||||
| 368 | sub render_links { | ||||||
| 369 | 0 | 1 | 0 | my ($self, $opt) | |||
| 370 | = validate(\@_, class_type(__PACKAGE__), | ||||||
| 371 | slurpy Dict[ | ||||||
| 372 | context => class_type('Yukki::Web::Context'), | ||||||
| 373 | links => ArrayRef[HashRef], | ||||||
| 374 | ]); | ||||||
| 375 | 0 0 | 0 0 | my ($ctx, $links) = @{$opt}{qw( context links )}; | ||||
| 376 | |||||||
| 377 | return $self->render( | ||||||
| 378 | template => $self->links_template, | ||||||
| 379 | context => $ctx, | ||||||
| 380 | vars => { | ||||||
| 381 | links => [ map { | ||||||
| 382 | 0 | 0 | +{ | ||||
| 383 | label => $_->{label}, | ||||||
| 384 | 0 | 0 | href => $ctx->rebase_url($_->{href}), | ||||
| 385 | } | ||||||
| 386 | } @$links ], | ||||||
| 387 | }, | ||||||
| 388 | ); | ||||||
| 389 | } | ||||||
| 390 | |||||||
| 391 - 401 | =head2 render
my $document = $self->render({
template => $template,
vars => { ... },
});
This renders the given L<Template::Pure>. The C<vars> are
used as the ones passed to the C<process> method.
=cut | ||||||
| 402 | |||||||
| 403 | sub render { | ||||||
| 404 | 6 | 1 | 2337 | my ($self, $opt) | |||
| 405 | = validate(\@_, class_type(__PACKAGE__), | ||||||
| 406 | slurpy Dict[ | ||||||
| 407 | template => class_type('Template::Pure'), | ||||||
| 408 | context => class_type('Yukki::Web::Context'), | ||||||
| 409 | vars => HashRef, | ||||||
| 410 | ]); | ||||||
| 411 | 6 6 | 39967 560 | my ($template, $ctx, $vars) = @{$opt}{qw( template context vars )}; | ||||
| 412 | 6 | 21 | $vars //= {}; | ||||
| 413 | |||||||
| 414 | 6 | 37 | my %vars = ( | ||||
| 415 | %$vars, | ||||||
| 416 | ctx => $ctx, | ||||||
| 417 | view => $self, | ||||||
| 418 | ); | ||||||
| 419 | |||||||
| 420 | 6 | 25 | return $template->render($vars); | ||||
| 421 | } | ||||||
| 422 | |||||||
| 423 | 1; | ||||||