File Coverage

lib/Su/Template.pm
Criterion Covered Total %
statement 231 235 98.3
branch 50 62 80.6
condition 21 27 77.7
subroutine 14 15 93.3
pod 3 4 75.0
total 319 343 93.0


line stmt bran cond sub pod time code
1             package Su::Template;
2              
3 25     25   25365 use strict;
  25         46  
  25         962  
4 25     25   131 use warnings;
  25         41  
  25         702  
5 25     25   129 use Exporter;
  25         45  
  25         1235  
6              
7 25     25   295 use File::Path;
  25         40  
  25         1878  
8 25     25   2323 use Data::Dumper;
  25         17979  
  25         1463  
9 25     25   135 use Test::More;
  25         48  
  25         315  
10 25     25   8524 use Carp;
  25         43  
  25         1751  
11 25     25   4371 use Fatal qw(open close);
  25         72481  
  25         150  
12              
13 25     25   41897 use Su::Log;
  25         73  
  25         11096  
14              
15             our @ISA = qw(Exporter);
16              
17             # render requires explicit use declaration.
18             our @EXPORT = qw(expand);
19             our @EXPORT_OK = qw(render);
20              
21             our $DEBUG = 0;
22              
23             # not used.
24             my $MODULE_PATH = __FILE__;
25              
26             # not used.
27             our $TEMPLATE_BASE_DIR = "./";
28              
29             # not used.
30             our $TEMPLATE_DIR = "Templates";
31              
32             =pod
33              
34             =head1 NAME
35              
36             Su::Template - A module to make the string using the specified template and passed parameters.
37              
38             =head1 SYNOPSIS
39              
40             my $tmpl = Su::Template->new;
41             my $str = $tmpl->expand( <<'HERE', $arg);
42             % my $arg = shift;
43             arg is <%=$arg%>
44             HERE
45              
46             =head1 DESCRIPTION
47              
48             Su::Template is a module to make the string using the specified template and passed parameters.
49              
50             =head1 AUTHOR
51              
52             lottz
53              
54             =head1 FUNCTIONS
55              
56             =over
57              
58             =cut
59              
60             =item new()
61              
62             A constructor.
63              
64             =cut
65              
66             sub new {
67 16     16 1 664 my $self = shift;
68              
69 16 50       64 my %h = @_ if @_;
70 16         130 my $log = Su::Log->new;
71 16         139 $h{logger} = $log;
72 16         80 return bless \%h, $self;
73             } ## end sub new
74              
75             sub import {
76 73     73   2114 my $self = shift;
77              
78             # Save import list and remove from hash.
79 73 50       314 my %tmp_h = @_ if @_;
80 73         157 my $imports_aref = $tmp_h{import};
81 73         145 delete $tmp_h{import};
82 73         304 my $base = $tmp_h{base};
83 73         125 my $dir = $tmp_h{dir};
84 73         375 Su::Log->trace( "base:" . Dumper($base) );
85 73         700 Su::Log->trace( "dir:" . Dumper($dir) );
86              
87             # print "base:" . Dumper($base) . "\n";
88             # print "dir:" . Dumper($dir) . "\n";
89              
90 73 50       556 $TEMPLATE_BASE_DIR = $base if $base;
91 73 50       388 $TEMPLATE_DIR = $dir if $dir;
92              
93 73 50 33     377 if ( $base || $dir ) {
94 0         0 $self->export_to_level( 1, $self, @{$imports_aref} );
  0         0  
95             } else {
96              
97             # If 'base' or 'dir' is not passed, then all of the parameters are required method names.
98 73         15490 $self->export_to_level( 1, $self, @_ );
99             }
100              
101             } ## end sub import
102              
103             =item expand()
104              
105             Expand the template using the passed context and return the result string.
106             Note that the keyword for here document must quoted by single quote. Double quote should cause unexpected error.
107              
108             Template syntax:
109              
110             <%= $val %> render the variable. Html special character will escaped.
111             <%== $val %> render the variable. Html special character will not escaped.
112             End tag ~%> discards line separator.
113             Expression surrounded by <% %> or the line start with '%' will parsed as Perl statements.
114              
115             If you want debug output, set the following flag on, then expand method return the debug string.
116              
117             $Su::Template::DEBUG=1;
118              
119             The functional style usage of this method is the following.
120              
121             $ret = Su::Template::expand(<<'__HERE__');
122             <% foreach my $v ("aa","bb","cc"){~%>
123             <%= $v%>
124             <%}~%>
125             xxx
126             yyy
127             zzz
128             __HERE__
129              
130             The OO style usage of this method is the following.
131              
132             my $tmpl = Su::Template->new;
133             my $str = $tmpl->expand( <<'HERE', $title, $link );
134             % my $title = shift;
135             % my $link = shift;
136             <%=$title%>
137             HERE
138              
139             The html special character in variable expression will be escaped like the following.
140              
141             $ret = $t->expand( <<'__HERE__', "aacc'dd\"ee&ff" );
142             % my $arg = shift;
143             <%= $arg ~%>
144             __HERE__
145              
146             is( $ret, "aa<bb>cc'dd"ee&ff" );
147              
148             Note that the html special character described in the raw part of the template will not be escaped.
149              
150             =cut
151              
152             my $escape_hash_str =
153             "my %escape_h = ( '<' => '<', '>' => '>', '\"' => '"', \"'\", => ''', '&' => '&',);";
154              
155             sub expand {
156 25     25   202 no warnings qw(redefine);
  25         48  
  25         38746  
157              
158             # TODO: If the pushed data to the list @f_t_a is a empty strng, then
159             # we should remove this process to simplify and optimize the
160             # method `make_template`. But whether add "\n" or not is rather
161             # complex, so optimization is a put-offed task.
162 60 100   60 1 9015 my $self = shift if ( ref $_[0] eq __PACKAGE__ );
163 60 50       460 my $DEBUG = $self->{debug} ? $self->{debug} : $DEBUG;
164              
165 60         91 my $org = shift;
166 60         129 my @args = @_;
167 60         114 my @ret = "";
168 60         84 my $b_perl_mode = 0;
169 60         81 my $b_no_last_newline = 0;
170 60         91 my $b_need_escape_hash = 0;
171 60         81 my $b_need_tmp_val = 0;
172              
173             # add dumy \n prepare for after pop. And set flag not to add \n the end of the line.
174 60 100       219 if ( substr( $org, ( length $org ) - 1 ) ne "\n" ) {
175 10         14 $org .= "\n";
176 10         12 $b_no_last_newline = 1;
177             }
178              
179 60         651 my @lines = split "\n", $org, -1;
180 60         132 pop @lines;
181 60         98 my $line_num = scalar @lines;
182 60         75 my $current_line = 0;
183 60         116 my $candidate_str = '';
184 60         289 Su::Log->trace( "org:" . Dumper($org) );
185 60         722 for my $l (@lines) {
186 1050         3579 Su::Log->trace( "loop:" . $l );
187 1050         8644 ++$current_line;
188 1050         1227 my $b_match = 0;
189 1050         1061 my $b_need_line_return = 1;
190 1050 100 100     6791 if ( $b_perl_mode && $l =~ /(.*)%>(\s*)$/ ) {
    100 100        
    100          
    100          
191 2         3 $b_perl_mode = 0;
192 2         2 $b_match = 1;
193 2         3 $b_need_line_return = 0;
194 2         5 push @ret, $1;
195 2         5 Su::Log->trace("perl mode out");
196             } elsif ($b_perl_mode) {
197 3         10 Su::Log->trace("is perl mode");
198 3         6 $b_match = 1;
199 3         3 $b_need_line_return = 0;
200 3         4 push @ret, $l;
201             } elsif ( substr( $l, 0, 1 ) eq '%' ) {
202 147         298 push @ret, substr( $l, 1 );
203 147         180 $b_match = 1;
204 147         259 $b_need_line_return = 0;
205             } elsif ( $l =~ /^\s*<%(.*)/ && index( $l, "%>" ) == -1 ) {
206 2         3 $b_perl_mode = 1;
207 2         3 $b_need_line_return = 0;
208 2         3 $b_match = 1;
209 2         5 push @ret, $1;
210 2         5 Su::Log->trace("perl mode in");
211             } else {
212 896         1010 my $prev_pos = 0;
213 896         2894 while ( $l =~ /<%((={0,2})(.*?)([~])?)%>/g ) {
214 144         179 $b_match = 1;
215              
216             # push @ret, ('push(@f_t_a, \'' . $`. '\');') if $`; # add previous part
217 144         457 my $tmp = $-[0];
218              
219 144         336 $candidate_str = substr( $l, $prev_pos, $tmp - $prev_pos );
220              
221             #note: Ensure matched varibe like $1 not effect outer scope.
222             {
223 144 50       168 $candidate_str =~ s/'/\\'/g if defined $candidate_str;
  144         388  
224             }
225 144 100       570 $candidate_str && push @ret,
226             ( 'push(@f_t_a, \'' . $candidate_str . '\');' )
227             ; # save from previous end pos to current start pos.
228 144 100 100     811 if ( defined $2 && ( $2 eq '=' or $2 eq '==' ) )
      33        
229             { # print variable itself
230 140         253 my $exp = $3;
231              
232 140         158 $b_need_tmp_val = 1;
233 140 100       384 if ( $2 eq '=' ) {
234 137         187 $b_need_escape_hash = 1;
235             }
236              
237             {
238 140         143 $exp =~ s/^ *(.*?) *$/$1/; # trim front and end of white space.
  140         810  
239             }
240              
241             # push @ret, ('push(@f_t_a, (' . $exp . '));'); # NOTE: other variables may be double quote not single quote like this like.
242              
243             # Escape and push.
244 140 100       335 if ( $2 eq '=' ) {
245 137         320 push @ret, '$tmp_val = ' . '(' . $exp . ');';
246              
247             # If the special charactor is already escaped using '&', then prevent unexpected double escaped.
248 137         254 push @ret, 'if($tmp_val){
249             $tmp_val=~s/&(?!(lt|gt|amp|quot|apos);)/&/go;
250             $tmp_val=~s/(<|>|\'|")/$escape_h{$1}/go;';
251              
252             # $tmp_val=~s/(<|>|\'|"|&)/$escape_h{$1}/go;';
253 137         232 push @ret, ( 'push(@f_t_a, $tmp_val' . ');}' );
254 137         221 push @ret, ('elsif(defined $tmp_val){push(@f_t_a, $tmp_val);}');
255             } else {
256              
257             # Push only. Not escape.
258 3         7 push @ret, '$tmp_val = ' . '(' . $exp . ');';
259 3         4 push @ret, 'if($tmp_val){';
260 3         4 push @ret, ( 'push(@f_t_a, $tmp_val' . ');}' );
261 3         6 push @ret, ('elsif(defined $tmp_val){push(@f_t_a, $tmp_val);}');
262              
263             } ## end else [ if ( $2 eq '=' ) ]
264              
265             } else {
266 4         7 push @ret, $3;
267             }
268 144         203 $prev_pos = pos($l);
269              
270 144 100 66     803 $b_need_line_return = 0 if ( defined $4 && $4 eq '~' );
271             } #while match
272              
273             # If match, register tail part. If not this condition, <% %>not include line will retisterred twice!
274 896 100       1768 if ($b_match) {
275 118         226 $candidate_str = substr( $l, $prev_pos, ( length $l ) - $prev_pos );
276              
277             #note: Ensure matched varibe like $1 not effect outer scope.
278             {
279 118 50       195 $candidate_str =~ s/'/\\'/g if defined $candidate_str;
  118         289  
280             }
281 118 100       335 $candidate_str && push @ret,
282             ( 'push(@f_t_a, \'' . $candidate_str . '\');' )
283             ; # save to the tail of the line.
284             } ## end if ($b_match)
285             } # else end
286              
287 1050   100     2468 my $no_need_newline = ( $line_num == $current_line ) && $b_no_last_newline;
288              
289             # Add new line to the make_template function itself. If the current line is comment, then we need new line to add \n to the @f_t_a array.
290 1050         1920 push @ret, "\n";
291              
292 1050 100       1782 if ($b_match) {
293 272         1103 Su::Log->trace("line b_match");
294              
295             # push @ret, ('push(@f_t_a, \'' . (($b_need_line_return && !$b_no_newline) ? "\n" : '') . '\');');# print eol.
296 272 100 100     1607 push @ret,
297             ( 'push(@f_t_a, \''
298             . ( ( $b_need_line_return && !$no_need_newline ) ? "\n" : '' )
299             . '\');' ); # print eol.
300             } else {
301 778         2324 Su::Log->trace("line not b_match");
302 778         1917 $l =~ s/'/\\'/g;
303              
304             # push @ret, ('push(@f_t_a, \'' . $l. (($b_need_line_return && !$b_no_newline) ? "\n" : '') . '\');');#print whole line.
305 778 100 66     3116 if ( !$b_need_line_return || $no_need_newline ) {
306 5 50       22 $l && push @ret, ( 'push(@f_t_a, \'' . $l . '\');' );
307             } else {
308              
309             #print whole line.
310 773         2690 push @ret, ( 'push(@f_t_a, \'' . $l . "\n" . '\');' );
311             }
312             } ## end else [ if ($b_match) ]
313              
314             # push @ret, "\n";
315             } #while line
316              
317 60 100       306 unshift( @ret, 'my $tmp_val="";' . "\n" )
318             if $b_need_tmp_val;
319 60 100       197 unshift( @ret, $escape_hash_str . "\n" )
320             if $b_need_escape_hash;
321              
322 60         103 unshift( @ret, 'sub make_template{' . "\n" . 'my @f_t_a=();' . "\n" );
323 60         101 push( @ret, 'return join(\'\',@f_t_a);' . "\n" . '}' );
324 60         569 my $prepare_data = join( '', @ret );
325 60 100   11 0 58005 eval($prepare_data);
  11 50       31  
  12         78  
  11         34  
  10         28  
  9         22  
  10         24  
  10         22  
  10         35  
  13         24  
  13         34  
  13         29  
  11         28  
  13         26  
  9         23  
  13         40  
  7         21  
  13         38  
  13         29  
  13         29  
  11         27  
  13         27  
  13         24  
  12         50  
  14         23  
  10         29  
  6         20  
  5         23  
  3         5  
  4         17  
  4         23  
  3         7  
  3         8  
  2         5  
  2         4  
  3         691  
  3         7  
  3         7  
  5         19  
  4         9  
  4         9  
  4         7  
  4         10  
  4         67  
  3         8  
  4         24  
  2         4  
  4         8  
  4         7  
  4         10  
  4         8  
  4         10  
  4         338  
  1         3  
  4         7  
  4         10  
  3         7  
  3         8  
  6         14  
  6         9  
  6         11  
  6         13  
  6         15  
  6         14  
  6         12  
  2         4  
  5         37  
  6         11  
  6         12  
  6         15  
  6         14  
  6         11  
  2         3  
  6         9  
  6         102  
  7         15  
  6         17  
  6         12  
  6         14  
  6         99  
  6         13  
  6         13  
  6         13  
  6         15  
  6         10  
  6         18  
  6         14  
  6         91  
  6         145  
  6         12  
  6         16  
  6         105  
  6         52  
326 60 50       458 $@ and die "[ERROR]Invalid Template format:" . $@ . "\n" . $prepare_data;
327              
328 60 50       154 if ($DEBUG) {
329 0         0 return $prepare_data; # return pre-eval data.
330             } else {
331 60         1848 return make_template(@args); # call evaled method.
332             }
333             } # eos expand()
334              
335             =item render()
336              
337             An alias to the method L.
338              
339             =cut
340              
341             sub render {
342 0     0 1 0 return expand(@_);
343             }
344              
345             =pod
346              
347             =back
348              
349             =cut
350              
351             1;
352