File Coverage

blib/lib/CodeGen/Cpppp/Template.pm
Criterion Covered Total %
statement 298 343 86.8
branch 85 140 60.7
condition 31 68 45.5
subroutine 44 57 77.1
pod 13 15 86.6
total 471 623 75.6


line stmt bran cond sub pod time code
1             package CodeGen::Cpppp::Template;
2              
3             our $VERSION = '0.005'; # VERSION
4             # ABSTRACT: Base class for template classes created by compiling cpppp
5              
6 17     17   226 use v5.20;
  17         99  
7 17     17   89 use warnings;
  17         30  
  17         925  
8 17     17   103 use Carp;
  17         80  
  17         1443  
9 17     17   101 use experimental 'signatures', 'lexical_subs', 'postderef';
  17         51  
  17         180  
10 17     17   3326 use Scalar::Util 'looks_like_number';
  17         49  
  17         1248  
11 17     17   11105 use Hash::Util;
  17         80333  
  17         124  
12 17     17   11747 use CodeGen::Cpppp::Output;
  17         56  
  17         838  
13 17     17   9647 use CodeGen::Cpppp::AntiCharacter;
  17         57  
  17         616  
14 17     17   114 use Exporter ();
  17         34  
  17         1258  
15             require version;
16              
17              
18             package CodeGen::Cpppp::Template::Exports {
19             use constant {
20 17         5844 PUBLIC => 'public',
21             PROTECTED => 'protected',
22             PRIVATE => 'private',
23 17     17   113 };
  17         35  
24             our @EXPORT_OK= qw( PUBLIC PROTECTED PRIVATE compile_cpppp format_commandline
25             format_timestamp
26             );
27             our %EXPORT_TAGS= (
28             'v0' => [qw( PUBLIC PROTECTED PRIVATE compile_cpppp )],
29             );
30             #sub util {
31             # return bless [ caller ], __PACKAGE__;
32             #}
33             #sub _caller { ref $_[0] eq __PACKAGE__? @{+shift} : caller(1) }
34             sub compile_cpppp {
35 1     1   12 my ($pkg, $filename, $line)= caller;
36 1         3 my $cpppp;
37 1 50       6 if (@_ == 1) {
38             # If the argument has any line terminator, assume it is cpppp code
39 1 50       8 if (index($_[0], "\n") >= 0) {
    50          
40 0         0 $cpppp= $_[0];
41             }
42             # if the argument is '__DATA__', read it from DATA
43             elsif ($_[0] eq '__DATA__') {
44 17     17   122 no strict 'refs';
  17         35  
  17         21747  
45 1         2 my $fh= *{${pkg}.'::DATA'};
  1         7  
46 1         94 my $pos= $fh->tell;
47 1         13559 local $/= undef;
48 1         33 $cpppp= <$fh>;
49             # now find out what line __DATA__ started on
50 1 50       3 eval {
51 1         36 $fh->seek(0,0);
52 1         34 $/= \$pos;
53 1         29 $line= 1 + scalar(()= <$fh> =~ /\n/g);
54             } or Carp::carp("Can't determine line number of __DATA__");
55 1         27 close $fh;
56             }
57             }
58 1 50       7 Carp::croak("compile_cppp argument should either be '__DATA__' or lines of cpppp code ending with '\\n'")
59             unless defined $cpppp;
60 1 50       5 Carp::croak("cpppp source cannot be empty")
61             unless length $cpppp;
62              
63 1         14 my $parse= CodeGen::Cpppp->new->parse_cpppp(\$cpppp, $filename, $line);
64 1         21 $pkg->_init_parse_data($parse);
65             $pkg->_build_BUILD_method(
66 1         7 $pkg->cpppp_version, $parse->{code}, $filename, $line);
67             }
68             sub format_commandline {
69 2     2   1348 require CodeGen::Cpppp::Platform;
70 2         12 CodeGen::Cpppp::Platform::format_commandline(@_);
71             }
72             sub format_timestamp {
73 0     0   0 my @t= gmtime;
74 0         0 sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ", $t[5]+1900, @t[4,3,2,1,0]
75             }
76             }
77              
78 25     25   68 sub _tag_for_version($ver) {
  25         62  
  25         44  
79 25         136 return ':v0';
80             }
81              
82             sub import {
83 45     45   184100 my $class= $_[0];
84 45         129 my $caller= caller;
85 45         240 for (my $i= 1; $i < @_; $i++) {
86 28 100       132 if ($_[$i] eq '-setup') {
87 25         303 my $ver= version->parse($_[$i+1]);
88 25         105 splice(@_, $i, 2, _tag_for_version($ver));
89 25         115 $class->_setup_derived_package($caller, $ver);
90             }
91             }
92 45         211 splice(@_, 0, 1, 'CodeGen::Cpppp::Template::Exports');
93 45         20735 goto \&Exporter::import;
94             }
95              
96             our $_next_pkg= 1;
97 24     24   54 sub _create_derived_package($class, $cpppp_ver, $parse_data) {
  24         67  
  24         40  
  24         41  
  24         70  
98 24         73 my $pkg= 'CodeGen::Cpppp::Template::_'.$_next_pkg++;
99 17     17   143 no strict 'refs';
  17         31  
  17         3982  
100 24         69 @{"${pkg}::ISA"}= ( $class );
  24         627  
101 24         72 ${"${pkg}::cpppp_version"}= $cpppp_ver;
  24         140  
102 24         215 $pkg->_init_parse_data($parse_data);
103             }
104              
105 25     25   43 sub _setup_derived_package($class, $pkg, $cpppp_ver) {
  25         45  
  25         40  
  25         41  
  25         89  
106 25         277 strict->import;
107 25         1022 warnings->import;
108 25         294 utf8->import;
109 25         281 experimental->import(qw( lexical_subs signatures postderef ));
110              
111 17     17   138 no strict 'refs';
  17         33  
  17         3546  
112 25 100       3885 @{"${pkg}::ISA"}= ( $class ) unless @{"${pkg}::ISA"};
  1         21  
  25         195  
113 25         47 ${"${pkg}::cpppp_version"}= $cpppp_ver;
  25         164  
114             }
115              
116 25     25   54 sub _init_parse_data($class, $parse_data) {
  25         51  
  25         36  
  25         33  
117 17     17   124 no strict 'refs';
  17         35  
  17         8209  
118 25         60 ${"${class}::_parse_data"}= $parse_data;
  25         232  
119             # Create accessors for all of the attributes declared in the template.
120 25         105 for (keys $parse_data->{template_parameter}->%*) {
121 15         27 my $name= $_;
122 15     8   49 *{"${class}::$name"}= sub { $_[0]{$name} };
  15         60  
  8         40  
123             }
124             # Expose all of the functions declared in the template
125 25         99 for (keys $parse_data->{template_method}->%*) {
126 5         12 my $name= $_;
127 5         75 *{"${class}::$name"}= sub {
128 4 50   4   2716 my $m= shift->{template_method}{$name}
129             or croak "Template execution did not define method '$name'";
130 4         15 goto $m;
131 5         28 };
132             }
133 25         116 $class;
134             }
135              
136 1     1 0 2 sub cpppp_version($class) {
  1         2  
  1         2  
137 17     17   176 no strict 'refs';
  17         32  
  17         5991  
138 1   33     28 ${"${class}::cpppp_version"} // __PACKAGE__->VERSION
  1         21  
139             }
140              
141 25     25   47 sub _gen_perl_scope_functions($class, $cpppp_ver) {
  25         39  
  25         49  
  25         40  
142             return (
143 25         1521 '# line '. (__LINE__+1) . ' "' . __FILE__ . '"',
144 0     0     'my sub param { unshift @_, $self; goto $self->can("_init_param") }',
  0            
145 0     0     'my sub define { unshift @_, $self; goto $self->can("define_template_macro") }',
  0            
146 0     0     'my sub section { unshift @_, $self; goto $self->can("current_output_section") }',
  0            
147 0     0     'my sub template { unshift @_, $self->context; goto $self->context->can("new_template") }',
  0            
148             'my $trim_comma= CodeGen::Cpppp::AntiCharacter->new(qr/,/, qr/\s*/);',
149             'my $trim_ws= CodeGen::Cpppp::AntiCharacter->new(qr/\s*/);',
150             );
151             }
152              
153 25     25   42 sub _gen_BUILD_method($class, $cpppp_ver, $perl, $src_filename, $src_lineno) {
  25         74  
  25         40  
  25         41  
  25         53  
  25         40  
  25         68  
154             return
155 25         202 "sub ${class}::BUILD(\$self, \$constructor_parameters=undef) {",
156             " Scalar::Util::weaken(\$self);",
157             # Inject all the lexical functions that need to be in scope
158             $class->_gen_perl_scope_functions($cpppp_ver),
159             qq{# line $src_lineno "$src_filename"},
160             $perl,
161             "}",
162             }
163              
164 1     1   2 sub _build_BUILD_method($class, $version, $perl, $src_filename, $src_lineno) {
  1         2  
  1         2  
  1         3  
  1         2  
  1         1  
  1         3  
165             {
166 17     17   124 no strict 'refs';
  17         39  
  17         15160  
  1         1  
167 1 50       3 croak "${class}::BUILD is already defined" if defined &{$class.'::BUILD'};
  1         8  
168             }
169 1 50       7 croak "Compile failed for ${class}::BUILD() : $@"
170             unless eval join "\n",
171             $class->_gen_BUILD_method($version, $perl, $src_lineno, $src_filename),
172             '1';
173             }
174              
175              
176 6     6 1 37 sub context { $_[0]{context} }
177              
178 36     36 1 5402 sub output { $_[0]->flush->{output} }
179              
180 13     13 1 31 sub current_output_section($self, $new=undef) {
  13         16  
  13         18  
  13         13  
181 13 50       26 if (defined $new) {
182 13 50       23 $self->output->has_section($new)
183             or croak "No defined output section '$new'";
184 13         37 $self->_finish_render;
185 13         22 $self->{current_output_section}= $new;
186             }
187 13         30 $self->{current_output_section};
188             }
189              
190              
191 0 0 0 0 1 0 sub autocolumn { $_[0]{autocolumn} = $_[1]||0 if @_ > 1; $_[0]{autocolumn} }
  0         0  
192 0 0 0 0 1 0 sub autocomma { $_[0]{autocomma} = $_[1]||0 if @_ > 1; $_[0]{autocomma} }
  0         0  
193 0 0 0 0 1 0 sub autoindent { $_[0]{autoindent} = $_[1]||0 if @_ > 1; $_[0]{autoindent} }
  0         0  
194 0 0 0 0 1 0 sub autostatementline { $_[0]{autostatementline}= $_[1]||0 if @_ > 1; $_[0]{autostatementline} }
  0         0  
195 219 50   219 1 380 sub indent { $_[0]{indent} = $_[1] if @_ > 1; $_[0]{indent} }
  219         362  
196 0 0 0 0 1 0 sub emit_POD { $_[0]{emit_POD} = $_[1]||0 if @_ > 1; $_[0]{emit_POD} }
  0         0  
197              
198 92     92   118 sub _parse_data($class) {
  92         123  
  92         127  
199 92 100       262 $class = ref $class if ref $class;
200 17     17   141 no strict 'refs';
  17         31  
  17         3805  
201 92         115 return ${"${class}::_parse_data"};
  92         402  
202             }
203              
204              
205 34     34 0 2969 sub new($class, @args) {
  34         66  
  34         57  
  34         49  
206 17     17   120 no strict 'refs';
  17         32  
  17         68487  
207 34 50 66     243 my %attrs= @args == 1 && ref $args[0]? $args[0]->%*
    100          
208             : !(@args&1)? @args
209             : croak "Expected even-length list or hashref";
210 34         116 my $parse= $class->_parse_data;
211             # Make sure each attr is the correct type of ref, for the params.
212 34         102 for (keys %attrs) {
213 34 100       130 if (my $p= $parse->{template_parameter}{$_}) {
214 10 100       47 if ($p eq '@') { ref $attrs{$_} eq 'ARRAY' or croak("Expected ARRAY for parameter $_"); }
  2 100       189  
    100          
215 3 100       205 elsif ($p eq '%') { ref $attrs{$_} eq 'HASH' or croak("Expected HASH for parameter $_"); }
216             }
217             else {
218 24 50       168 croak("Unknown parameter '$_' to template $parse->{filename}")
219             unless $class->can($_);
220             }
221             }
222              
223             my $self= bless {
224             autocomma => 1,
225             autostatementline => 1,
226             (map +($_ => $parse->{$_}||0), qw(
227             autoindent autocolumn convert_linecomment_to_c89
228             )),
229             indent => $parse->{indent},
230 32   100     480 output => CodeGen::Cpppp::Output->new,
231             current_output_section => 'private',
232             %attrs,
233             }, $class;
234             Scalar::Util::weaken($self->{context})
235 32 100       235 if $self->{context};
236 32         473 $self->BUILD(\%attrs);
237 31         313 $self->flush;
238             }
239              
240              
241 8     8 1 19 sub coerce_parameters($class, $params) {
  8         20  
  8         15  
  8         14  
242 8         37 my %ret;
243 8         46 my $parse= $class->_parse_data;
244 8         38 for my $k (keys $parse->{template_parameter}->%*) {
245 0         0 my $p= $parse->{template_parameter}{$k};
246 0   0     0 my $v= $params->{$p.$k} // $params->{$k};
247 0 0       0 next unless defined $v;
248 0 0       0 if ($p eq '@') {
    0          
249 0 0       0 $v= ref $v eq 'HASH'? [ keys %$v ] : [ $v ]
    0          
250             unless ref $v eq 'ARRAY';
251             } elsif ($p eq '%') {
252             # If it isn't a hash, treat it like a list that needs added to a set
253 0 0       0 $v= { map +($_ => 1), ref $v eq 'ARRAY'? @$v : ($v) }
    0          
254             unless ref $v eq 'HASH';
255             }
256 0         0 $ret{$k}= $v;
257             }
258 8         25 \%ret;
259             }
260              
261 30     30   54 sub _init_param($self, $name, $ref, @initial_value) {
  30         35  
  30         34  
  30         35  
  30         36  
  30         28  
262 30 100       59 if (exists $self->{$name}) {
263             # Assign the value received from constructor to the variable in the template
264             ref $ref eq 'SCALAR'? ($$ref= $self->{$name})
265 1 50       6 : ref $ref eq 'ARRAY' ? (@$ref= @{$self->{$name} || []})
266 8 50       34 : ref $ref eq 'HASH' ? (%$ref= %{$self->{$name} || {}})
  2 50       16  
    100          
    100          
267             : croak "Unhandled ref type ".ref($ref);
268             } else {
269 22 50       71 ref $ref eq 'SCALAR'? ($$ref= $initial_value[0])
    100          
    100          
270             : ref $ref eq 'ARRAY' ? (@$ref= @initial_value)
271             : ref $ref eq 'HASH' ? (%$ref= @initial_value)
272             : croak "Unhandled ref type ".ref($ref);
273             }
274            
275             # Now store the variable of the template directly into this hash
276             ref $ref eq 'SCALAR'? Hash::Util::hv_store(%$self, $name, $$ref)
277 30 100       113 : ($self->{$name}= $ref);
278 30         65 $ref;
279             }
280              
281              
282 70     70 1 97 sub flush($self) {
  70         105  
  70         81  
283 70         229 $self->_finish_render;
284 70         312 $self;
285             }
286              
287              
288 0     0 1 0 sub define_template_macro($self, $name, $code) {
  0         0  
  0         0  
  0         0  
  0         0  
289 0         0 $self->{template_macro}{$name}= $code;
290             }
291              
292              
293 5     5 1 59 sub define_template_method($self, $name, $code) {
  5         9  
  5         11  
  5         8  
  5         8  
294 5         61 $self->{template_method}{$name}= $code;
295             }
296              
297 0     0   0 sub _render_pod_block($self, $i) {
  0         0  
  0         0  
  0         0  
298 0 0       0 if ($self->emit_POD) {
299 0         0 $self->_finish_render;
300 0         0 $self->{output}->append($self->{current_output_section} => $self->_parse_data->{pod_blocks}[$i]);
301             }
302             }
303              
304 83     83   102 sub _finish_render($self) {
  83         112  
  83         87  
305 83 100       187 return unless defined $self->{current_out};
306             # Second pass, adjust whitespace of all column markers so they line up.
307             # Iterate from leftmost column rightward.
308 65         83 for my $group_i (sort { $a <=> $b } keys %{$self->{current_out_colgroup_state}}) {
  1         4  
  65         246  
309             delete $self->{current_out_colgroup_state}{$group_i}
310 4 50       14 if $self->{current_out_colgroup_state}{$group_i} == 2;
311 4         8 my $token= _colmarker($group_i);
312             # Find the longest prefix (excluding trailing whitespace)
313             # Also find the max number of digits following column.
314 4         12 my ($maxcol, $maxdigit)= (0,0);
315 4         5 my ($linestart, $col);
316 4         257 while ($self->{current_out} =~ /[ ]* $token (-? 0x[A-Fa-f0-9]+ | -? \d+)? /gx) {
317 20         72 $linestart= rindex($self->{current_out}, "\n", $-[0])+1;
318 20         38 $col= $-[0] - $linestart;
319 20 100       39 $maxcol= $col if $col > $maxcol;
320 20 100 100     146 $maxdigit= length $1 if defined $1 && length $1 > $maxdigit;
321             }
322 4         150 $self->{current_out} =~ s/[ ]* $token (?= (-? 0x[A-Fa-f0-9]+ | -? \d+)? )/
323 20         60 $linestart= rindex($self->{current_out}, "\n", $-[0])+1;
324 20 100       133 " "x(1 + $maxcol - ($-[0] - $linestart) + ($1? $maxdigit - length($1) : 0))
325             /gex;
326             }
327 65         242 $self->{output}->append($self->{current_output_section} => $self->{current_out});
328 65         143 $self->{current_out}= '';
329             }
330              
331 13     13   15 sub _colmarker($colgroup_id) { join '', "\x{200A}", map chr(0x2000+$_), split //, $colgroup_id; }
  13         20  
  13         13  
  13         117  
332 0 0 0 0   0 sub _str_esc { join '', map +(ord($_) > 0x7e || ord($_) < 0x21? sprintf("\\x{%X}",ord) : $_), split //, $_[0] }
333              
334             sub _render_code_block {
335 50     50   374 my ($self, $i, @expr_subs)= @_;
336 50         100 my $block= $self->_parse_data->{code_block_templates}[$i];
337 50         91 my $text= $block->{text};
338             # Continue appending to the same output buffer so that autocolumn can
339             # inspect the result as a whole.
340 50   100     171 my $out= \($self->{current_out} //= '');
341 50         85 my $at= 0;
342 50         81 my %colmarker;
343             # @subst contains a list of positions in the template body where text
344             # may need inserted.
345 50         60 for my $s (@{$block->{subst}}) {
  50         137  
346 153         337 $$out .= substr($text, $at, $s->{pos} - $at);
347 153         210 $at= $s->{pos} + $s->{len};
348             # Column marker - may substitute for whitespace during _finish_render
349 153 100       314 if ($s->{colgroup}) {
    50          
350 20   66     51 my $mark= $colmarker{$s->{colgroup}} //= _colmarker($s->{colgroup});
351 20         32 $$out .= $mark;
352 20 100       40 $self->{current_out_colgroup_state}{$s->{colgroup}}= $s->{last}? 2 : 1;
353             }
354             # Variable interpolation - insert value of one of the @expr_subs here
355             elsif (defined $s->{eval_idx}) {
356 133 50       223 my $fn= $expr_subs[$s->{eval_idx}]
357             or die;
358             # Identify the indent settings at this point so that other modules can
359             # automatically generate matching code.
360 133         2601 my ($last_char)= ($$out =~ /(\S) (\s*) \Z/x);
361 133         300 my $cur_line= substr($$out, rindex($$out, "\n")+1);
362 133         769 (my $indent_prefix= $cur_line) =~ s/\S/ /g;
363 133         180 local $CodeGen::Cpppp::CURRENT_INDENT_PREFIX= $indent_prefix;
364 133 100       265 local $CodeGen::Cpppp::INDENT= $self->indent if defined $self->indent;
365             # it is "inline" context if non-whitespace occurs on this line already
366 133         231 my $is_inline= !!($cur_line =~ /\S/);
367 133         157 local $CodeGen::Cpppp::CURRENT_IS_INLINE= $is_inline;
368              
369             # Avoid using $_ up to this point so that $_ pases through
370             # from the surrounding code into the evals
371 133         213 my @out= $fn->($self, $out);
372             # Expand arrayref and coderefs in the returned list
373 133 50 66     456 @out= @{$out[0]} if @out == 1 && ref $out[0] eq 'ARRAY';
  0         0  
374 133   33     289 ref eq 'CODE' && ($_= $_->($self, $out)) for @out;
375 133         268 @out= grep defined, @out;
376             # Now decide how to join this into the code template.
377             # If this interpolation does not occur at the beginning of the line,
378 133         156 my $join_sep= $";
379             # Special handling if the user requested a list substitution
380 133 100       262 if (ord $s->{eval} == ord '@') {
381 11 50       24 $last_char= '' unless defined $last_char;
382 11 100 100     107 if ($self->{autostatementline} && ($last_char eq '{' || $last_char eq ';')
    50 66        
    0 100        
      66        
      33        
      0        
      0        
383             && substr($text, $s->{pos}+$s->{len}, 1) eq ';'
384             ) {
385 3         16 @out= grep /\S/, @out; # remove items that are only whitespace
386 3 50 33     13 if (!$is_inline && substr($text, $s->{pos}+$s->{len}, 2) eq ";\n") {
387 3         6 $join_sep= ";\n";
388             # If no elements, remove the whole line.
389 3 100       6 if (!@out) {
390 2         23 $$out =~ s/[ \t]+\Z//;
391 2         3 $at+= 2; # skip over ";\n"
392             }
393             } else {
394 0         0 $join_sep= "; ";
395             }
396             }
397             elsif ($self->{autocomma} && ($last_char eq ',' || $last_char eq '(' || $last_char eq '{')) {
398 8         32 @out= grep /\S/, @out; # remove items that are only whitespace
399 8 100       17 $join_sep= $is_inline? ', ' : ",\n";
400             # If no items, or the first nonwhitespace character is a comma,
401             # remove the previous comma
402 8 100 66     33 if (!@out || $out[0] =~ /^\s*,/) {
403 2         9 $$out =~ s/,(\s*)\Z/$1/;
404             }
405             }
406             elsif ($self->{autoindent} && !$is_inline && $join_sep !~ /\n/) {
407 0         0 $join_sep .= "\n";
408             }
409             }
410 133 100       220 if (@out) {
411             # 'join' doesn't respect concat magic on AntiCharacter :-(
412 128         168 my $str= shift @out;
413 128         178 $str .= $join_sep . $_ for @out;
414             # Autoindent: if new text contains newline, add current indent to start of each line.
415 128 100 66     315 if ($self->{autoindent} && length $indent_prefix) {
416 120         204 $str =~ s/\n/\n$indent_prefix/g;
417             }
418 128         381 $$out .= $str;
419             }
420             }
421             }
422 50         366 $$out .= substr($text, $at);
423             }
424              
425             1;
426              
427             __END__