File Coverage

blib/lib/CodeGen/Cpppp/Template.pm
Criterion Covered Total %
statement 224 250 89.6
branch 73 110 66.3
condition 28 53 52.8
subroutine 32 35 91.4
pod 0 8 0.0
total 357 456 78.2


line stmt bran cond sub pod time code
1             package CodeGen::Cpppp::Template;
2              
3             our $VERSION = '0.001_04'; # TRIAL VERSION
4             # ABSTRACT: Template objects created by parsing and compiling cpppp
5              
6 7     7   88 use v5.20;
  7         36  
7 7     7   36 use warnings;
  7         14  
  7         184  
8 7     7   37 use Carp;
  7         24  
  7         458  
9 7     7   87 use experimental 'signatures', 'postderef';
  7         20  
  7         43  
10 7     7   1144 use Scalar::Util 'looks_like_number';
  7         17  
  7         460  
11 7     7   4803 use Hash::Util;
  7         21545  
  7         42  
12 7     7   3596 use CodeGen::Cpppp::Output;
  7         18  
  7         308  
13 7     7   2950 use CodeGen::Cpppp::AntiCharacter;
  7         21  
  7         194  
14 7     7   44 use Exporter ();
  7         15  
  7         203  
15             require version;
16              
17             use constant {
18 7         2519 PUBLIC => 'public',
19             PROTECTED => 'protected',
20             PRIVATE => 'private',
21 7     7   37 };
  7         17  
22             our @EXPORT_OK= qw( PUBLIC PROTECTED PRIVATE );
23             our %EXPORT_TAGS= (
24             'v0' => \@EXPORT_OK,
25             );
26 15     15   28 sub _tag_for_version($ver) {
  15         25  
  15         22  
27 15         60 return ':v0';
28             }
29              
30             sub import {
31 22     22   54 my $class= $_[0];
32 22         56 my $caller= caller;
33 22         95 for (my $i= 1; $i < @_; $i++) {
34 15 50       64 if ($_[$i] eq '-setup') {
35 15         197 my $ver= version->parse($_[$i+1]);
36 15         91 splice(@_, $i, 2, _tag_for_version($ver));
37 15         57 $class->_setup_derived_package($caller, $ver);
38             }
39             }
40 22         47495 goto \&Exporter::import;
41             }
42              
43             our $_next_pkg= 1;
44 15     15   33 sub _create_derived_package($class, $version, $parse_data) {
  15         35  
  15         25  
  15         25  
  15         23  
45 15         43 my $pkg= 'CodeGen::Cpppp::Template::_'.$_next_pkg++;
46 7     7   53 no strict 'refs';
  7         15  
  7         2785  
47 15         35 @{"${pkg}::ISA"}= ( $class );
  15         360  
48 15         53 ${"${pkg}::_parse_data"}= $parse_data;
  15         104  
49             # Create accessors for all of the attributes declared in the template.
50 15         69 for (keys $parse_data->{template_parameter}->%*) {
51 13         28 my $name= $_;
52 13     8   39 *{"${pkg}::$name"}= sub { $_[0]{$name} };
  13         97  
  8         37  
53             }
54             # Expose all of the functions declared in the template
55 15         57 for (keys $parse_data->{template_method}->%*) {
56 3         7 my $name= $_;
57 3         32 *{"${pkg}::$name"}= sub {
58 2 50   2   1445 my $m= shift->{template_method}{$name}
59             or croak "Template execution did not define method '$name'";
60 2         7 goto $m;
61 3         15 };
62             }
63 15         52 $pkg;
64             }
65              
66 15     15   30 sub _setup_derived_package($class, $pkg, $version) {
  15         28  
  15         20  
  15         23  
  15         20  
67 15         95 strict->import;
68 15         131 warnings->import;
69 15         79 utf8->import;
70 15         153 experimental->import(qw( lexical_subs signatures postderef ));
71              
72 7     7   56 no strict 'refs';
  7         14  
  7         1469  
73 15 50       2010 @{"${pkg}::ISA"}= ( $class ) unless @{"${pkg}::ISA"};
  0         0  
  15         141  
74             }
75              
76 15     15   35 sub _gen_perl_scope_functions($class, $version) {
  15         25  
  15         25  
  15         20  
77             return (
78 15         418 '# line '. __LINE__ . ' "' . __FILE__ . '"',
79             'my sub param { unshift @_, $self; goto $self->can("_init_param") }',
80             'my sub define { unshift @_, $self; goto $self->can("define_template_macro") }',
81             'my sub section { unshift @_, $self; goto $self->can("current_output_section") }',
82             'my sub template { unshift @_, $self->context; goto $self->context->can("new_template") }',
83             'my $trim_comma= CodeGen::Cpppp::AntiCharacter->new(qr/,/, qr/\s*/);',
84             'my $trim_ws= CodeGen::Cpppp::AntiCharacter->new(qr/\s*/);',
85             );
86             }
87              
88 61     61   93 sub _parse_data($class) {
  61         93  
  61         80  
89 61 100       147 $class = ref $class if ref $class;
90 7     7   54 no strict 'refs';
  7         26  
  7         910  
91 61         93 return ${"${class}::_parse_data"};
  61         279  
92             }
93              
94 6     6 0 41 sub context { $_[0]{context} }
95              
96 24     24 0 1644 sub new($class, @args) {
  24         43  
  24         43  
  24         32  
97 7     7   49 no strict 'refs';
  7         14  
  7         19303  
98 24 50 66     159 my %attrs= @args == 1 && ref $args[0]? $args[0]->%*
    100          
99             : !(@args&1)? @args
100             : croak "Expected even-length list or hashref";
101 24         87 my $parse= $class->_parse_data;
102             # Make sure each attr is the correct type of ref, for the params.
103 24         72 for (keys %attrs) {
104 18 100       45 if (my $p= $parse->{template_parameter}{$_}) {
105 10 100       56 if ($p eq '@') { ref $attrs{$_} eq 'ARRAY' or croak("Expected ARRAY for parameter $_"); }
  2 100       237  
    100          
106 3 100       110 elsif ($p eq '%') { ref $attrs{$_} eq 'HASH' or croak("Expected HASH for parameter $_"); }
107             }
108             else {
109 8 50       44 croak("Unknown parameter '$_' to template $parse->{filename}")
110             unless $class->can($_);
111             }
112             }
113              
114             my $self= bless {
115 22         237 (map +($_ => $parse->{$_}), qw(
116             autocomma autoindent autostatementline autocolumn
117             context
118             )),
119             output => CodeGen::Cpppp::Output->new,
120             current_output_section => 'private',
121             %attrs,
122             }, $class;
123             Scalar::Util::weaken($self->{context})
124 22 100       137 if $self->{context};
125 22         197 $self->BUILD(\%attrs);
126 22         226 $self;
127             }
128              
129 0     0 0 0 sub coerce_parameters($class, $params) {
  0         0  
  0         0  
  0         0  
130 0         0 my %ret;
131 0         0 my $parse= $class->_parse_data;
132 0         0 for my $k (keys $parse->{template_parameter}->%*) {
133 0         0 my $p= $parse->{template_parameter}{$k};
134 0   0     0 my $v= $params->{$p.$k} // $params->{$k};
135 0 0       0 next unless defined $v;
136 0 0       0 if ($p eq '@') {
    0          
137 0 0       0 $v= ref $v eq 'HASH'? [ keys %$v ] : [ $v ]
    0          
138             unless ref $v eq 'ARRAY';
139             } elsif ($p eq '%') {
140             # If it isn't a hash, treat it like a list that needs added to a set
141 0 0       0 $v= { map +($_ => 1), ref $v eq 'ARRAY'? @$v : ($v) }
    0          
142             unless ref $v eq 'HASH';
143             }
144 0         0 $ret{$k}= $v;
145             }
146 0         0 \%ret;
147             }
148              
149 9     9 0 17 sub current_output_section($self, $new=undef) {
  9         13  
  9         14  
  9         14  
150 9 50       21 if (defined $new) {
151 9 50       24 $self->output->has_section($new)
152             or croak "No defined output section '$new'";
153 9         28 $self->_finish_render;
154 9         18 $self->{current_output_section}= $new;
155             }
156 9         20 $self->{current_output_section};
157             }
158              
159 30     30 0 45 sub flush($self) {
  30         42  
  30         44  
160 30         97 $self->_finish_render;
161 30         132 $self;
162             }
163              
164 27     27 0 2317 sub output($self) {
  27         44  
  27         37  
165 27         108 $self->flush->{output};
166             }
167              
168 28     28   63 sub _init_param($self, $name, $ref, @initial_value) {
  28         45  
  28         36  
  28         38  
  28         53  
  28         43  
169 28 100       64 if (exists $self->{$name}) {
170             # Assign the value received from constructor to the variable in the template
171             ref $ref eq 'SCALAR'? ($$ref= $self->{$name})
172 1 50       5 : ref $ref eq 'ARRAY' ? (@$ref= @{$self->{$name} || []})
173 8 50       36 : ref $ref eq 'HASH' ? (%$ref= %{$self->{$name} || {}})
  2 50       12  
    100          
    100          
174             : croak "Unhandled ref type ".ref($ref);
175             } else {
176 20 50       65 ref $ref eq 'SCALAR'? ($$ref= $initial_value[0])
    100          
    100          
177             : ref $ref eq 'ARRAY' ? (@$ref= @initial_value)
178             : ref $ref eq 'HASH' ? (%$ref= @initial_value)
179             : croak "Unhandled ref type ".ref($ref);
180             }
181            
182             # Now store the variable of the template directly into this hash
183             ref $ref eq 'SCALAR'? Hash::Util::hv_store(%$self, $name, $$ref)
184 28 100       106 : ($self->{$name}= $ref);
185 28         64 $ref;
186             }
187              
188 0     0 0 0 sub define_template_macro($self, $name, $code) {
  0         0  
  0         0  
  0         0  
  0         0  
189 0         0 $self->{template_macro}{$name}= $code;
190             }
191              
192 3     3 0 26 sub define_template_method($self, $name, $code) {
  3         5  
  3         6  
  3         5  
  3         5  
193 3         24 $self->{template_method}{$name}= $code;
194             }
195              
196 39     39   50 sub _finish_render($self) {
  39         57  
  39         50  
197 39 100       100 return unless defined $self->{current_out};
198             # Second pass, adjust whitespace of all column markers so they line up.
199             # Iterate from leftmost column rightward.
200 30         48 for my $group_i (sort { $a <=> $b } keys %{$self->{current_out_colgroup_state}}) {
  1         7  
  30         126  
201             delete $self->{current_out_colgroup_state}{$group_i}
202 4 50       15 if $self->{current_out_colgroup_state}{$group_i} == 2;
203 4         10 my $token= _colmarker($group_i);
204             # Find the longest prefix (excluding trailing whitespace)
205             # Also find the max number of digits following column.
206 4         13 my ($maxcol, $maxdigit)= (0,0);
207 4         7 my ($linestart, $col);
208 4         177 while ($self->{current_out} =~ /[ ]* $token (-? 0x[A-Fa-f0-9]+ | -? \d+)? /gx) {
209 20         102 $linestart= rindex($self->{current_out}, "\n", $-[0])+1;
210 20         67 $col= $-[0] - $linestart;
211 20 100       40 $maxcol= $col if $col > $maxcol;
212 20 100 100     169 $maxdigit= length $1 if defined $1 && length $1 > $maxdigit;
213             }
214 4         135 $self->{current_out} =~ s/[ ]* $token (?= (-? 0x[A-Fa-f0-9]+ | -? \d+)? )/
215 20         124 $linestart= rindex($self->{current_out}, "\n", $-[0])+1;
216 20 100       187 " "x(1 + $maxcol - ($-[0] - $linestart) + ($1? $maxdigit - length($1) : 0))
217             /gex;
218             }
219 30         127 $self->{output}->append($self->{current_output_section} => $self->{current_out});
220 30         65 $self->{current_out}= '';
221             }
222              
223 13     13   22 sub _colmarker($colgroup_id) { join '', "\x{200A}", map chr(0x2000+$_), split //, $colgroup_id; }
  13         20  
  13         16  
  13         125  
224 0 0 0 0   0 sub _str_esc { join '', map +(ord($_) > 0x7e || ord($_) < 0x21? sprintf("\\x{%X}",ord) : $_), split //, $_[0] }
225              
226             sub _render_code_block {
227 37     37   263 my ($self, $i, @expr_subs)= @_;
228 37         99 my $block= $self->_parse_data->{code_block_templates}[$i];
229 37         80 my $text= $block->{text};
230 37   100     147 my $out= \($self->{current_out} //= '');
231 37         66 my $at= 0;
232 37         61 my %colmarker;
233             my $prev_colmark;
234             # First pass, perform substitutions and record new column markers
235 37         61 my $subst= $block->{subst};
236 37         91 for (my $i= 0; $i < @$subst; $i++) {
237 139         240 my $s= $subst->[$i];
238 139         339 $$out .= substr($text, $at, $s->{pos} - $at);
239 139         233 $at= $s->{pos} + $s->{len};
240 139 100       324 if ($s->{colgroup}) {
    50          
241 20   66     62 my $mark= $colmarker{$s->{colgroup}} //= _colmarker($s->{colgroup});
242 20         40 $$out .= $mark;
243 20         29 $prev_colmark= $s;
244 20 100       64 $self->{current_out_colgroup_state}{$s->{colgroup}}= $s->{last}? 2 : 1;
245             }
246             elsif (defined $s->{eval_idx}) {
247 119 50       255 my $fn= $expr_subs[$s->{eval_idx}]
248             or die;
249             # Avoid using $_ up to this point so that $_ pases through
250             # from the surrounding code into the evals
251 119         256 my @out= $fn->($self, $out);
252             # Expand arrayref and coderefs in the returned list
253 119 50 66     489 @out= @{$out[0]} if @out == 1 && ref $out[0] eq 'ARRAY';
  0         0  
254 119   33     313 ref eq 'CODE' && ($_= $_->($self, $out)) for @out;
255 119         284 @out= grep defined, @out;
256             # Now decide what to join them with.
257 119         214 my $join_sep= $";
258 119         167 my $indent= '';
259 119         3617 my ($last_char)= ($$out =~ /(\S) (\s*) \Z/x);
260 119         364 my $cur_line= substr($$out, rindex($$out, "\n")+1);
261 119         259 my $inline= $cur_line =~ /\S/;
262 119 50       244 if ($self->{autoindent}) {
263 119         747 ($indent= $cur_line) =~ s/\S/ /g;
264             }
265             # Special handling if the user requested a list substitution
266 119 100       285 if (ord $s->{eval} == ord '@') {
267 11 50       26 $last_char= '' unless defined $last_char;
268 11 100 100     124 if ($self->{autostatementline} && ($last_char eq '{' || $last_char eq ';')
    50 66        
    0 100        
      66        
      33        
      0        
      0        
269             && substr($text, $s->{pos}+$s->{len}, 1) eq ';'
270             ) {
271 3         10 @out= grep /\S/, @out; # remove items that are only whitespace
272 3 50 33     28 if (!$inline && substr($text, $s->{pos}+$s->{len}, 2) eq ";\n") {
273 3         8 $join_sep= ";\n";
274             # If no elements, remove the whole line.
275 3 100       8 if (!@out) {
276 2         33 $$out =~ s/[ \t]+\Z//;
277 2         5 $at+= 2; # skip over ";\n"
278             }
279             } else {
280 0         0 $join_sep= "; ";
281             }
282             }
283             elsif ($self->{autocomma} && ($last_char eq ',' || $last_char eq '(' || $last_char eq '{')) {
284 8         35 @out= grep /\S/, @out; # remove items that are only whitespace
285 8 100       22 $join_sep= $inline? ', ' : ",\n";
286             # If no items, or the first nonwhitespace character is a comma,
287             # remove the previous comma
288 8 100 66     37 if (!@out || $out[0] =~ /^\s*,/) {
289 2         13 $$out =~ s/,(\s*)\Z/$1/;
290             }
291             }
292             elsif ($self->{autoindent} && !$inline && $join_sep !~ /\n/) {
293 0         0 $join_sep .= "\n";
294             }
295             }
296 119 100       223 if (@out) {
297             # 'join' doesn't respect concat magic on AntiCharacter :-(
298 114         172 my $str= shift @out;
299 114         248 $str .= $join_sep . $_ for @out;
300             # Autoindent: if new text contains newline, add current indent to start of each line.
301 114 100 66     350 if ($self->{autoindent} && $indent) {
302 108         235 $str =~ s/\n/\n$indent/g;
303             }
304 114         419 $$out .= $str;
305             }
306             }
307             }
308 37         260 $$out .= substr($text, $at);
309             }
310              
311             1;
312              
313             __END__