File Coverage

blib/lib/Text/Macro.pm
Criterion Covered Total %
statement 289 298 96.9
branch 86 116 74.1
condition 9 15 60.0
subroutine 40 41 97.5
pod 5 11 45.4
total 429 481 89.1


line stmt bran cond sub pod time code
1             ########################################
2             # Module: Text::Macro
3             # Purpose: A text macro-language almost identical to Text::FastTemplate
4             # Author: Michael Maraist
5             # Derived from: Text::FastTemplate
6             ########################################
7              
8             package Text::Macro;
9 1     1   7257 use strict;
  1         1  
  1         31  
10 1     1   5 use warnings;
  1         1  
  1         28  
11 1     1   839 use integer;
  1         13  
  1         3  
12 1     1   692 use IO::File;
  1         10056  
  1         187  
13              
14             our %cache;
15              
16             require 5.006;
17             our $VERSION = '0.07';
18              
19 1     1   844 use fields qw( filename path code src stack blocks switch_stack line subs for_sep );
  1         1519  
  1         6  
20              
21             ########################################
22             # Function: new
23             ########################################
24             sub new {
25 19     19 1 5580 my $self = shift;
26 19         66 my %args = @_;
27 19 50       52 my $filename = $args{file} or die "No file-name provided";
28 19         27 my $path = "";
29 19 50       51 if ( exists $args{path} ) {
30 19         29 $path = $args{path};
31 19 50       46 if ( ! $path =~ m!/$! ) {
32 0         0 $path .= "/";
33             }
34             }
35              
36 19 100       121 if ( exists $cache{ $filename } ) {
37 3         9 return $cache{ $filename };
38             }
39              
40 16         48 my Text::Macro $this = fields::new( $self );
41              
42 16         5106 $this->{filename} = $filename;
43 16         27 $this->{path} = $path;
44              
45 16         37 $this->parse( );
46 16         44 $cache{ $filename } = $this;
47 16         62 return $this;
48             } # end new
49              
50             ########################################
51             # Function: readFile
52             # Purpose: recursive file-reader. Actively handles include-directives
53             # IN: file-name
54             # Throws: couldn't open file
55             ########################################
56             sub readFile($$)
57             {
58 19     19 0 23 my Text::Macro $this = shift;
59 19         23 my $file = shift;
60 19         36 my $file_name = $this->{path} . $file;
61              
62 19 50       89 my $fh = new IO::File $file_name
63             or die "Could not load file: $file_name";
64 19         1402 my $idx = 1;
65 270 100       2321 return map {
66 19         485 /^\s*\#include\s+(.*)\n$/ ? $this->readFile( $1 ) : [ $_, $idx++, $file ];
67             } $fh->getlines();
68             } # end readFile
69              
70             ########################################
71             #
72             ########################################
73             sub compileError($$)
74             {
75 0     0 0 0 my ( $msg, $block ) = @_;
76 0         0 print STDERR "Error: $msg; $block->[1] $block->[2]: $block->[0]";
77 0         0 exit(-1);
78             } # end compileError
79              
80             ########################################
81             # Function: getText
82             # Purpose: get the next raw string piece of text
83             ########################################
84             sub getTextBlock($)
85             {
86 164     164 0 178 my ( $blocks ) = @_;
87 164         134 my $line_chunks;
88             # extract line chunks (consolidating with previous line if possible.
89 164         224 $_ = $blocks->[ $#$blocks ];
90 164 100       296 if ( ref( $_ ) ne "ARRAY" ) {
91 80         102 $line_chunks = [];
92 80         124 push @$blocks, $line_chunks;
93             } else {
94 84         103 $line_chunks = $_;
95             }
96              
97 164         261 return $line_chunks;
98             } # end getTextBlock
99              
100             ########################################
101             # Function: concatText
102             # Purpose: concatenate text to an existing print-statement
103             ########################################
104             sub concatText($$)
105             {
106 194     194 0 242 my ( $line_block, $txt ) = @_;
107 194         235 $txt =~ s/\'/\\\'/g;
108 194         222 my $str = $line_block->[-1];
109 194 100 100     658 if ( defined $str && substr( $str, -1, 1 ) eq "'") {
110             # previous piece of text was string; concat
111 82         112 substr( $str, -1, 0, $txt );
112 82         428 $line_block->[-1] = $str;
113             } else {
114 112         592 push @$line_block, "'$txt'";
115             }
116             } # end concatText
117              
118             ##################################################
119             # Function: parseSection
120             # Purpose: convert the input blocks file into perl code
121             # ToDo: Make sure that the input is properly escaped
122             ##################################################
123             sub parseSection($@)
124             {
125 30     30 0 36 my Text::Macro $this = shift;
126 30         59 my @lines = @_;
127              
128 30         598 for ( my $line_idx = 0; $line_idx <= $#lines ; $line_idx++ ) {
129 265         296 my $line_block = $lines[$line_idx];
130              
131 265         318 my $line = $line_block->[0];
132              
133             # Determine if we're a command
134 265 100       984 if ( my ( $cmd, $cond ) = $line =~ /
135             ^ \s*\#( if | for | elsif | else | endif | endfor | comment | sub | callsub | pre | switch | case | endswitch | default | set )\b \s* (.*) \n $
136             /x ) {
137             # We are a command
138 104 100       549 if ( $cmd eq "if" ) {
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    0          
139 15 50       26 compileError( "No if conditional", $line_block )
140             unless $cond;
141              
142             {
143 1     1   952 no warnings;
  1         2  
  1         1094  
  15         17  
144             # pre-process cond
145 15         73 $cond =~ s/\#\#(\w+)([\[\{][^\#]+)?\#\#/\$scope->{$1}$2/gs;
146             }
147              
148 15         18 push @{$this->{blocks}}, "if ( $cond ) {\n";
  15         46  
149 15         16 push @{$this->{stack}}, "i";
  15         28  
150 15         52 next;
151             } elsif ( $cmd eq "set" ) {
152 4 50       10 compileError( "No if conditional", $line_block )
153             unless $cond;
154 4         10 my ( $var, $val ) = split( "=", $cond );
155 4 50       9 compileError( "Need a variable", $line_block )
156             unless $var;
157              
158 4         6 my @txt;
159 4         14 for ( split( /(\#\#[\w\{\[\}\]]+\#\#)/, $val ) ) {
160 8         17 my ( $var_name, $ref_data ) = /^\#\#(\w+)([\[\{][^\#]+)?\#\#$/;
161 8 100       13 if ( $var_name ) {
162 2 100       8 $ref_data = "" unless defined $ref_data;
163 2         6 push @txt, "\$scope->{$var_name}$ref_data";
164             } else {
165 6         24 push @txt, '"' . quotemeta( $_ ) . '"';
166             }
167             }
168 4         6 push @{$this->{blocks}}, "\$scope->{$var} = ", join( " . ", @txt ) ,";";
  4         30  
169             } elsif ( $cmd eq "switch" ) {
170 5         15 my ( $var ) = $cond =~ /\#\#(\w+)\#\#/;
171 5 50       10 compileError( "No switch argument", $line_block )
172             unless $var;
173              
174 5         4 push @{$this->{switch_stack}}, [ $var, 1 ];
  5         14  
175 5         5 push @{$this->{stack}}, "s";
  5         17  
176             } elsif ( $cmd eq "endswitch" ) {
177 5 50 66     22 compileError( "Exiting $this->{stack}[-1] when still in switch", $line_block )
178             unless $this->{stack}[-1] eq "s" || $this->{stack}[-1] eq "d";
179 5         4 pop @{$this->{stack}};
  5         7  
180 5 100       14 push @{$this->{blocks}}, "}\n" unless $this->{switch_stack}[-1][1];
  3         4  
181 5         4 pop @{$this->{switch_stack}};
  5         17  
182             } elsif ( $cmd eq "case" ) {
183 6         21 my ( @syms ) = $cond =~ /(\"[^\"]+\")/g;
184 6 50       11 compileError( "No conditional value", $line_block )
185             unless @syms;
186 6         15 compileError( "case with no switch", $line_block )
187 6 50       71 unless @{$this->{switch_stack}};
188 6         10 my $switch_item = $this->{switch_stack}[-1];
189 6         7 my $sym_str = "(" . join( ') || (', map { "\$scope->{$switch_item->[0]} eq $_" } @syms ) . ")";
  6         20  
190 6 100       12 if ( $switch_item->[1] ) {
191 3         3 push @{$this->{blocks}}, "if ( $sym_str ) {\n";
  3         9  
192 3         11 $switch_item->[1] = 0;
193             } else {
194 3         3 push @{$this->{blocks}}, "} elsif ( $sym_str ) {\n";
  3         14  
195             }
196             } elsif ( $cmd eq "default" ) {
197 4         12 compileError( "case with no switch", $line_block )
198 4 50       4 unless @{$this->{switch_stack}};
199 4         6 my $switch_item = $this->{switch_stack}[-1];
200 4 100       8 if ( $switch_item->[1] ) {
201             } else {
202 3         4 push @{$this->{blocks}}, "} else {\n";
  3         6  
203             }
204 4         14 $this->{stack}[-1] = "d";
205             } elsif ( $cmd eq "pre" ) {
206 0         0 my $line_chunks;
207 0         0 $line_chunks = getTextBlock($this->{blocks});
208             # Gobble up the remainder of the sub for later use
209 0   0     0 for ( $line_idx++ ; $line_idx <= $#lines && ( ( $line = ($line_block = $lines[$line_idx])->[0] ) !~ /^\s*\#endpre/ ) ; $line_idx++ ) {
210 0         0 concatText( $line_chunks, $line );
211             }
212              
213             } elsif ( $cmd eq "sub" ) {
214 10         30 my ($sub_name) = $cond =~ /(\w+)/;
215 10 50       20 compileErrot( "No sub-name", $line_block )
216             unless $sub_name;
217              
218 10         12 my @sub_data = ();
219              
220             # Gobble up the remainder of the sub for later use
221 10   66     63 for ( $line_idx++ ;
222             $line_idx <= $#lines && ( ( $line = ($line_block = $lines[$line_idx])->[0] ) !~ /^\s*\#endsub/ ) ;
223             $line_idx++ ) {
224 32         166 push @sub_data, $line_block;
225             }
226 10         51 $this->{subs}{$sub_name} = \@sub_data;
227             } elsif ( $cmd eq "callsub" ) {
228 14         50 my ($sub_name, $params) = $cond =~ /(\w+)\s*(\S.*)?/;
229 14 50       27 compileError( "No sub-name", $line_block )
230             unless $sub_name;
231              
232 14 50       35 compileError( "sub-macro '$sub_name' not defined", $line_block )
233             unless exists $this->{subs}{$sub_name};
234              
235 14 100       22 if ( $params ) {
236 1     1   6 no warnings;
  1         2  
  1         437  
237             # pre-process cond
238 7         20 $params =~ s/\#\#(\w+)([\[\{][^\#]+)?\#\#/\$scope->{$1}$2/gs;
239              
240 7         9 push @{$this->{blocks}}, "{ my \$save_argv = \$scope->{ARGV};\n\$scope->{ARGV} = [$params];\n";
  7         20  
241             }
242              
243 14         15 $this->parseSection( @{$this->{subs}{$sub_name}} );
  14         74  
244 14 100       54 if ( $params ) {
245 7         7 push @{$this->{blocks}}, "\n\$scope->{ARGV} = \$save_argv;\n}\n";
  7         27  
246             }
247             } elsif ( $cmd eq "for" ) {
248 7 50       38 my ($var) = $cond =~ /\#\#(\w+)\#\#/
249             or compileError( "No conditional", $line_block );
250             #$this->{for_sep} = "";
251 7 100       21 if ( $cond =~ /; sep="(.*?)"/ ) {
252 3         4 push @{$this->{for_sep}}, "(\$counter == \@loop_var ? \"\" : \"$1\" )";
  3         12  
253             } else {
254 4         5 push @{$this->{for_sep}}, "";
  4         10  
255             }
256 7         6 push @{$this->{stack}}, "f";
  7         15  
257             #ZZZ make size, idx, and comma exist only if used
258 7         9 push @{$this->{blocks}}, <
  7         49  
259             \{ my \$old_scope = \$scope;
260             my \@loop_var = (exists \$scope->{$var} && ref(\$scope->{$var}) eq "ARRAY" ) ? \@{\$scope->{$var}} : ();
261             my \$counter = 0;
262             for my \$el ( \@loop_var ) \{
263             \$scope = defined(\$el) && ref(\$el) eq "HASH" ? { \%\$el } : {};
264             for my \$scope_name ( grep { !exists \$scope->{ \$_ } } keys \%\$old_scope ) {
265             \$scope->{ \$scope_name } = \$old_scope->{ \$scope_name };
266             }
267             \$scope->{${var}_SIZE} = scalar \@loop_var;
268             \$scope->{${var}_IDX} = ++\$counter;
269              
270             EOS
271             } elsif ( $cmd eq "else" ) {
272 8         24 compileError( "else not level with if", $line_block )
273 8 50       14 unless $this->{stack}[ $#{$this->{stack}} ] eq "i";
274              
275 8         12 $this->{stack}[ $#{$this->{stack}} ] = "e"; # pop/push
  8         14  
276 8         9 push @{$this->{blocks}}, "} else {\n";
  8         31  
277             } elsif ( $cmd eq "elsif" ) {
278 4 50       11 compileError( "No conditional", $line_block )
279             unless $cond;
280 4         12 compileError( "elsif not level with if", $line_block )
281 4 50       6 unless $this->{stack}[ $#{$this->{stack}} ] eq "i";
282              
283             # pre-process cond
284             #Note that this searches for either ##var## or ##var[x][y]## or ##var{x}{y}##.
285             {
286 1     1   6 no warnings;
  1         2  
  1         1001  
  4         6  
287 4         27 $cond =~ s/\#\#(\w+)([\[\{][^\#]+)?\#\#/\$scope->{$1}$2/sg;
288             }
289              
290              
291 4         6 push @{$this->{blocks}}, "} elsif ( $cond ) {\n";
  4         23  
292             } elsif ( $cmd eq "endif" ) {
293 15         26 $_ = $this->{stack}[ $#{$this->{stack}} ];
  15         28  
294 15 50 66     115 compileError( "endif not level with if", $line_block )
295             unless $_ eq "i" || $_ eq "e";
296 15         15 pop @{$this->{stack}};
  15         20  
297 15         21 push @{$this->{blocks}}, "}\n";
  15         66  
298             } elsif ( $cmd eq "endfor" ) {
299 7         21 compileError( "endfor not level with for", $line_block )
300 7 50       11 unless $this->{stack}[ $#{$this->{stack}} ] eq "f";
301 7         8 pop @{$this->{stack}};
  7         8  
302 7 100       20 if ( length $this->{for_sep}[-1] ) {
303 3         7 my $line_block = getTextBlock( $this->{blocks} );
304 3         7 push @$line_block, $this->{for_sep}[-1];
305             #concatText( $line_block, $for_sep );
306             }
307 7         8 push @{$this->{blocks}}, "}\n\$scope=\$old_scope;\n}\n";
  7         14  
308 7         8 pop @{$this->{for_sep}};
  7         28  
309             } elsif ( $cmd eq "comment" ) {
310             # ignore line
311             } else {
312 0         0 compileError( "Invalid command state", $line_block );
313             }
314             } else { # if command
315             # We weren't a command
316 161         321 my $line_chunks = getTextBlock( $this->{blocks} );
317              
318             # replace "##\w+[x]##" with a variable insertion point or raw text
319 161         429 for ( split( /(\#\#[\w\{\[\}\]]+\#\#)/, $line ) ) {
320 227         369 my ( $var_name, $ref_data ) = /^\#\#(\w+)([\[\{][^\#]+)?\#\#$/;
321 227 100       307 if ( $var_name ) {
322 33 100       58 $ref_data = "" unless defined $ref_data;
323 33         77 push @$line_chunks, "\$scope->{$var_name}$ref_data";
324             } else {
325 194         286 concatText( $line_chunks, $_ );
326             }
327             }
328             } # end else (not command)
329             } # end readlines
330              
331             } # end parseSection
332              
333              
334             ##################################################
335             # Function: parse
336             # Purpose: convert the input template file into perl code
337             # ToDo: Make sure that the input is properly escaped
338             ##################################################
339             sub parse($)
340             {
341 16     16 0 22 my Text::Macro $this = shift;
342              
343 16         40 my @lines = $this->readFile( $this->{filename} );
344              
345 16         75 $this->{for_sep} = [];
346 16         24 $this->{blocks} = [];
347 16         24 $this->{stack} = [];
348 16         24 $this->{switch_stack} = [];
349 16         26 $this->{subs} = {};
350              
351 16         48 $this->parseSection( @lines );
352              
353 16         41 die "stack not unraveled at end of input"
354 16 50       18 if @{$this->{stack}};
355              
356             # Convert text-chunks to print statements
357 16         15 for my $block ( @{$this->{blocks}} ) {
  16         32  
358 174 100       306 if ( ref( $block ) eq "ARRAY" ) {
359 80         82 for( @$block ) {
360 148         195 s!\\\n!!;
361             }
362 80         203 $block = "\$fh->( " . join( ", ", @$block ) . ");\n";
363             }
364             }
365              
366 16         57 my @code = (
367             "sub {\nno warnings;\nmy \$tmp = shift; my \$scope = { \%\$tmp };\nmy \$fh = shift;\n",
368 16         21 @{$this->{blocks}},
369             "};\n"
370             );
371 16         77 my $src = "@code";
372 16         30 $this->{src} = $src;
373             #print "CODE{$this->{src}\nCODE}\n";
374 1     1   7 my $code = eval $src;
  1     1   1  
  1     1   982  
  1     1   8  
  1     1   2  
  1     1   291  
  1     1   8  
  1     1   2  
  1     1   313  
  1     1   5  
  1     1   2  
  1     1   53  
  1     1   7  
  1     1   2  
  1     1   219  
  1     1   6  
  1         2  
  1         110  
  1         8  
  1         3  
  1         79  
  1         7  
  1         2  
  1         80  
  1         7  
  1         2  
  1         158  
  1         6  
  1         1  
  1         163  
  1         4  
  1         2  
  1         188  
  1         5  
  1         2  
  1         139  
  1         5  
  1         1  
  1         232  
  1         5  
  1         1  
  1         54  
  1         6  
  1         2  
  1         156  
  1         4  
  1         2  
  1         95  
  16         1126  
375              
376 16         196 $this->{code} = $code;
377             } # end parse
378              
379             ########################################
380             # Function: print
381             ########################################
382             sub print($$) {
383 1     1 1 8 my Text::Macro $this = shift;
384 1         2 my $data = shift;
385 1 50       6 die "Compilation error"
386             unless $this->{code};
387 1     1   7 no warnings;
  1         3  
  1         125  
388 1     1   31 $this->{code}->( $data, sub { print @_; });
  1         28  
389             } # end print
390              
391             sub pipe($$$) {
392 2     2 1 250 my Text::Macro $this = shift;
393 2         3 my $data = shift;
394 2         3 my $fh = shift;
395              
396 2 50       7 die "Compilation error"
397             unless $this->{code};
398 1     1   5 no warnings;
  1         2  
  1         205  
399 2     2   50 $this->{code}->( $data, sub { print $fh @_; } );
  2         19  
400             } # end pipe
401              
402             sub toFile($$$) {
403 1     1 1 10 my Text::Macro $this = shift;
404 1         2 my $data = shift;
405 1         2 my $file_name = shift;
406              
407 1 50       6 my $fh = new IO::File ">$file_name"
408             or die "Could not open file: $file_name";
409              
410 1         88 $this->pipe( $data, $fh );
411              
412 1         5 $fh->close();
413             } # end toFile
414              
415             sub toString($$) {
416 16     16 1 370 my ( $this, $data ) = @_;
417              
418 16 50       49 die "Compilation error"
419             unless $this->{code};
420              
421 16         16 my @contents;
422 1     1   6 no warnings;
  1         1  
  1         102  
423 16     78   432 $this->{code}->( $data, sub { push @contents, @_; } );
  78         1485  
424 16         125 return join( "", @contents );
425             } # end toString
426              
427             1;
428              
429             __END__