File Coverage

blib/lib/Statocles/Template.pm
Criterion Covered Total %
statement 84 94 89.3
branch 24 30 80.0
condition 8 10 80.0
subroutine 15 16 93.7
pod 4 4 100.0
total 135 154 87.6


line stmt bran cond sub pod time code
1             package Statocles::Template;
2             our $VERSION = '0.084';
3             # ABSTRACT: A template object to pass around
4              
5 60     60   4796 use Statocles::Base 'Class';
  60         131  
  60         507  
6 60     60   443038 use Mojo::Template;
  60         806619  
  60         517  
7 60     60   2342 use Scalar::Util qw( blessed );
  60         148  
  60         2919  
8 60     60   365 use Storable qw( dclone );
  60         126  
  60         30133  
9              
10             #pod =attr content
11             #pod
12             #pod The main template string. This will be generated by reading the file C<path> by
13             #pod default.
14             #pod
15             #pod =cut
16              
17             has content => (
18             is => 'ro',
19             isa => Str,
20             lazy => 1,
21             default => sub {
22             my ( $self ) = @_;
23             return Path::Tiny->new( $self->path )->slurp;
24             },
25             );
26              
27             #pod =attr path
28             #pod
29             #pod The path to the file for this template. Optional.
30             #pod
31             #pod =cut
32              
33             has path => (
34             is => 'ro',
35             isa => Str,
36             coerce => sub {
37             return "$_[0]"; # Force stringify in case of Path::Tiny objects
38             },
39             );
40              
41             #pod =attr theme
42             #pod
43             #pod The theme this template was created from. Used for includes and other
44             #pod information.
45             #pod
46             #pod =cut
47              
48             has theme => (
49             is => 'ro',
50             isa => Theme,
51             coerce => Theme->coercion,
52             );
53              
54             #pod =attr include_stores
55             #pod
56             #pod An array of L<stores|Statocles::Store> to look for includes. Will be
57             #pod used in addition to the L<include_stores from the
58             #pod Theme|Statocles::Theme/include_stores>.
59             #pod
60             #pod =cut
61              
62             has include_stores => (
63             is => 'ro',
64             isa => ArrayRef[Store],
65             default => sub { [] },
66             coerce => sub {
67             my ( $thing ) = @_;
68             if ( ref $thing eq 'ARRAY' ) {
69             return [ map { Store->coercion->( $_ ) } @$thing ];
70             }
71             return [ Store->coercion->( $thing ) ];
72             },
73             );
74              
75             has _template => (
76             is => 'ro',
77             isa => InstanceOf['Mojo::Template'],
78             lazy => 1,
79             default => sub {
80             my ( $self ) = @_;
81             my $t = Mojo::Template->new(
82             name => $self->path,
83             );
84             $t->parse( $self->content );
85             return $t;
86             },
87             );
88              
89             #pod =method BUILDARGS
90             #pod
91             #pod Set the default path to something useful for in-memory templates.
92             #pod
93             #pod =cut
94              
95             around BUILDARGS => sub {
96             my ( $orig, $self, @args ) = @_;
97             my $args = $self->$orig( @args );
98             if ( !$args->{path} ) {
99             my ( $i, $caller_class ) = ( 0, (caller 0)[0] );
100             while ( $caller_class->isa( 'Statocles::Template' )
101             || $caller_class->isa( 'Sub::Quote' )
102             || $caller_class->isa( 'Method::Generate::Constructor' )
103             ) {
104             #; say "Class: $caller_class";
105             $i++;
106             $caller_class = (caller $i)[0];
107             }
108             #; say "Class: $caller_class";
109             $args->{path} = join " line ", (caller($i))[1,2];
110             }
111             return $args;
112             };
113              
114             #pod =method render
115             #pod
116             #pod my $html = $tmpl->render( %args )
117             #pod
118             #pod Render this template, passing in %args. Each key in %args will be available as
119             #pod a scalar in the template.
120             #pod
121             #pod =cut
122              
123             sub render {
124 3021     3021 1 405467 my ( $self, %args ) = @_;
125 3021         46286 my $t = $self->_template;
126 3021         61835 $t->prepend( $self->_prelude( '_tmpl', keys %args ) );
127              
128 3021         19526 my $content;
129             {
130             # Add the helper subs, like Mojolicious::Plugin::EPRenderer does
131 60     60   466 no strict 'refs';
  60         138  
  60         2046  
  3021         4325  
132 60     60   322 no warnings 'redefine';
  60         117  
  60         58695  
133              
134             # Add theme helpers first, to ensure default helpers do not get
135             # overridden.
136 3021 100       11009 if ( $self->theme ) {
137 2242         3347 my %theme_helpers = %{ $self->theme->_helpers };
  2242         8693  
138 2242         6431 for my $helper ( keys %theme_helpers ) {
139 8         14 *{"@{[$t->namespace]}::$helper"} = sub {
  8         22  
140 7     7   3715 $theme_helpers{ $helper }->( \%args, @_ );
141 8         45 };
142             }
143             }
144              
145             # Add default helpers
146 3021         5061 local *{"@{[$t->namespace]}::include"} = sub {
  3021         8013  
147 284 50   284   16947 if ( $_[0] eq '-raw' ) {
148 0         0 return $self->include( @_ );
149             }
150 284         515 my ( $name, %extra_args ) = @_;
151 284         608 my $inner_tmpl = $self->include( $name );
152 282   100     1442 return $inner_tmpl->render( %args, %extra_args ) || '';
153 3021         15312 };
154              
155 3021         4813 local *{"@{[$t->namespace]}::markdown"} = sub {
  3021         6806  
156 6     6   13134 my ( $text, %extra_args ) = @_;
157             die "Cannot use markdown helper: No site object given to template"
158 6 100       52 unless exists $args{site};
159 5         49 return $args{site}->markdown->markdown( $text );
160 3021         38647 };
161              
162 3021         4867 local *{"@{[$t->namespace]}::content"} = sub {
  3021         6476  
163 976     976   477559 my ( $section, $content ) = @_;
164 976 100       3686 if ( $content ) {
    100          
165 63 100       217 if ( ref $content eq 'CODE' ) {
166 60         142 $content = $content->();
167             }
168 63         9259 $args{page}->_content_sections->{ $section } = $content;
169 63         500 return;
170             }
171             elsif ( $section ) {
172 107   100     1836 return $args{page}->_content_sections->{ $section } // '';
173             }
174 806         3970 return $args{content};
175 3021         24912 };
176              
177 3021         23199 $content = $t->process( \%args );
178             }
179              
180 3021 100 66     577022 if ( blessed $content && $content->isa( 'Mojo::Exception' ) ) {
181 4         27 die "Error in template: " . $content;
182             }
183 3017         15129 return $content;
184             }
185              
186             # Build the Perl string that will unpack the passed-in args
187             # This is how Mojolicious::Plugin::EPRenderer does it, but I'm probably
188             # doing something wrong here...
189             sub _prelude {
190 3021     3021   8663 my ( $self, @vars ) = @_;
191             return join " ",
192             'use strict; use warnings; no warnings "ambiguous";',
193             'my $vars = shift;',
194 3021         6942 map( { "my \$$_ = \$vars->{'$_'};" } @vars ),
  19633         59333  
195             ;
196             }
197              
198             #pod =method include
199             #pod
200             #pod my $tmpl = $tmpl->include( $path );
201             #pod my $tmpl = $tmpl->include( @path_parts );
202             #pod
203             #pod Get the desired L<template|Statocles::Template> to include based on the given
204             #pod C<path> or C<path_parts>. Looks through all the L<include_stores|/include_stores>.
205             #pod If nothing is found, looks in the L<theme includes|Statocles::Theme/include>.
206             #pod
207             #pod =cut
208              
209             sub include {
210 284     284 1 563 my ( $self, @path ) = @_;
211 284         419 my $render = 1;
212 284 50       574 if ( $path[0] eq '-raw' ) {
213             # Allow raw files to not be passed through the template renderer
214             # This override flag will always exist, but in the future we may
215             # add better detection to possible file types to process
216 0         0 $render = 0;
217 0         0 shift @path;
218             }
219 284         1026 my $path = Path::Tiny->new( @path );
220              
221 284         8526 my @stores = @{ $self->include_stores };
  284         692  
222 284         409 for my $store ( @{ $self->include_stores } ) {
  284         608  
223 6 100       29 if ( $store->has_file( $path ) ) {
224 3 50       195 if ( $render ) {
225 3         22 return $self->theme->build_template( $path, $store->read_file( $path ) );
226             }
227 0         0 return $store->read_file( $path );
228             }
229             }
230              
231 281         669 my $include = eval {
232 281 50       1204 $self->theme->include( !$render ? ( '-raw', @path ) : @path );
233             };
234 281 100 66     14878 if ( $@ && $@ =~ /^Can not find include/ ) {
235             die qq{Can not find include "$path" in include directories: }
236 2         150 . join( ", ", map { sprintf q{"%s"}, $_->path } @stores, @{ $self->theme->include_stores }, $self->theme->store )
  4         31  
  2         28  
237             . "\n";
238             }
239              
240 279         661 return $include;
241             }
242              
243             #pod =method merge_state
244             #pod
245             #pod $tmpl->merge_state( $state );
246             #pod
247             #pod Merge the given C<$state> hash reference into the existing. Keys
248             #pod in C<$state> override those in L<the state attribute|/state>.
249             #pod
250             #pod =cut
251              
252             sub merge_state {
253 0     0 1 0 my ( $self, $new_state ) = @_;
254 0         0 for my $key ( keys %$new_state ) {
255 0         0 my $value = $new_state->{ $key };
256 0 0       0 $value = dclone $value if ref $value;
257 0         0 $self->state->{ $key } = $value;
258             }
259 0         0 return;
260             }
261              
262             #pod =method coercion
263             #pod
264             #pod my $coerce = Statocles::Template->coercion;
265             #pod
266             #pod has template => (
267             #pod is => 'ro',
268             #pod isa => InstanceOf['Statocles::Template'],
269             #pod coerce => Statocles::Template->coercion,
270             #pod );
271             #pod
272             #pod A class method to returns a coercion sub to convert strings into template
273             #pod objects.
274             #pod
275             #pod =cut
276              
277             sub coercion {
278 60     60 1 41420 my ( $class ) = @_;
279             return sub {
280 5859 100   5859   267239 die "Template is undef" unless defined $_[0];
281 5858 100       86622 return !ref $_[0]
282             ? Statocles::Template->new( content => $_[0] )
283             : $_[0]
284             ;
285 60         486 };
286             }
287              
288             1;
289              
290             __END__
291              
292             =pod
293              
294             =encoding UTF-8
295              
296             =head1 NAME
297              
298             Statocles::Template - A template object to pass around
299              
300             =head1 VERSION
301              
302             version 0.084
303              
304             =head1 DESCRIPTION
305              
306             This is the template abstraction layer for Statocles.
307              
308             =head1 ATTRIBUTES
309              
310             =head2 content
311              
312             The main template string. This will be generated by reading the file C<path> by
313             default.
314              
315             =head2 path
316              
317             The path to the file for this template. Optional.
318              
319             =head2 theme
320              
321             The theme this template was created from. Used for includes and other
322             information.
323              
324             =head2 include_stores
325              
326             An array of L<stores|Statocles::Store> to look for includes. Will be
327             used in addition to the L<include_stores from the
328             Theme|Statocles::Theme/include_stores>.
329              
330             =head1 METHODS
331              
332             =head2 BUILDARGS
333              
334             Set the default path to something useful for in-memory templates.
335              
336             =head2 render
337              
338             my $html = $tmpl->render( %args )
339              
340             Render this template, passing in %args. Each key in %args will be available as
341             a scalar in the template.
342              
343             =head2 include
344              
345             my $tmpl = $tmpl->include( $path );
346             my $tmpl = $tmpl->include( @path_parts );
347              
348             Get the desired L<template|Statocles::Template> to include based on the given
349             C<path> or C<path_parts>. Looks through all the L<include_stores|/include_stores>.
350             If nothing is found, looks in the L<theme includes|Statocles::Theme/include>.
351              
352             =head2 merge_state
353              
354             $tmpl->merge_state( $state );
355              
356             Merge the given C<$state> hash reference into the existing. Keys
357             in C<$state> override those in L<the state attribute|/state>.
358              
359             =head2 coercion
360              
361             my $coerce = Statocles::Template->coercion;
362              
363             has template => (
364             is => 'ro',
365             isa => InstanceOf['Statocles::Template'],
366             coerce => Statocles::Template->coercion,
367             );
368              
369             A class method to returns a coercion sub to convert strings into template
370             objects.
371              
372             =head1 TEMPLATE LANGUAGE
373              
374             The default Statocles template language is Mojolicious's Embedded Perl
375             template. Inside the template, every key of the %args passed to render() will
376             be available as a simple scalar:
377              
378             # template.tmpl
379             % for my $p ( @$pages ) {
380             <%= $p->{content} %>
381             % }
382              
383             my $tmpl = Statocles::Template->new( path => 'template.tmpl' );
384             $tmpl->render(
385             pages => [
386             { content => 'foo' },
387             { content => 'bar' },
388             ]
389             );
390              
391             =head1 DEFAULT HELPERS
392              
393             The following functions are available to the template by default.
394              
395             =head2 content
396              
397             The content helper gets and sets content sections, including the main content.
398              
399             %= content
400             <%= content %>
401              
402             With no arguments, C<content> will get the main content of the template.
403             This will be the HTML from the document or page.
404              
405             % content section_name => begin
406             Section Content
407             % end
408             <% content section_name => "Section Content" %>
409              
410             With two arguments, save the content into the named section. This will
411             be saved in the template L<state attribute|/state>, which can be copied
412             to other templates (like the layout template).
413              
414             %= content 'section_name'
415             <%= content 'section_name' %>
416              
417             With one argument, gets the content previously stored with the given
418             section name. This comes from L<the state attribute|/state>.
419              
420             =head2 include
421              
422             %= include 'path/file.html.ep'
423             %= include 'path/file.markdown', var => 'value'
424              
425             Include a file into this one. The file will be parsed as a template and
426             given the same variables as the current template. Optionally, additional
427             name-value pairs can be given to the included template. These additional
428             template variables override any current variables.
429              
430             Includes will be searched for in the L<Theme's C<include_stores>
431             attribute|Statocles::Theme/include_stores>. For content documents
432             rendered by the L<Statocles::Page::Document
433             class|Statocles::Page::Document>, this includes the document's parent
434             directory.
435              
436             Including markdown files does not automatically translate them into
437             HTML. If you're in a page template or layout template, use the
438             L<markdown helper|/markdown> to render the markdown into HTML.
439              
440             =head2 markdown
441              
442             %= markdown $markdown_text
443             %= markdown $app->{data}{description}
444             %= markdown include 'path/include.markdown'
445              
446             Render the given markdown text into HTML. This is useful for allowing users to
447             write markdown in L<site data|Statocles::Site/data>, and L<app data|Statocles::App/data> in
448             the L<configuration file|Statocles::Help::Config/data>,
449             or L<document data|Statocles::Document/data> attributes in the document frontmatter.
450              
451             Combining the C<markdown> and L<include|/include> helpers allows for adding
452             template directives to any included markdown file.
453              
454             =head1 SEE ALSO
455              
456             =over 4
457              
458             =item L<Statocles::Help::Theme>
459              
460             =item L<Statocles::Theme>
461              
462             =back
463              
464             =head1 AUTHOR
465              
466             Doug Bell <preaction@cpan.org>
467              
468             =head1 COPYRIGHT AND LICENSE
469              
470             This software is copyright (c) 2016 by Doug Bell.
471              
472             This is free software; you can redistribute it and/or modify it under
473             the same terms as the Perl 5 programming language system itself.
474              
475             =cut