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; |