File Coverage

blib/lib/Text/Forge.pm
Criterion Covered Total %
statement 172 219 78.5
branch 43 94 45.7
condition 14 24 58.3
subroutine 33 42 78.5
pod 0 13 0.0
total 262 392 66.8


line stmt bran cond sub pod time code
1             package Text::Forge;
2              
3             $Debug = 0;
4              
5 5     5   2169 use strict;
  5         9  
  5         215  
6 5         418 use vars qw(
7             $VERSION $AUTOLOAD $CWD %URI_ESCAPES %HTML_ENTITIES $Debug
8 5     5   22 );
  5         4  
9 5     5   22 use Carp;
  5         14  
  5         339  
10 5     5   2658 use Symbol qw();
  5         4244  
  5         133  
11 5     5   57 use Cwd;
  5         9  
  5         352  
12 5     5   287946 use HTTP::Headers;
  5         49842  
  5         208  
13 5     5   3255 use File::PathConvert;
  5         14500  
  5         342  
14 5     5   2790 use Text::Forge::MemCache;
  5         13  
  5         13057  
15              
16             $VERSION = '0.26';
17              
18             $CWD = Cwd::fastcwd();
19              
20             # Build mappings from URI and HTML encodings
21             # Taken from URI::Escape and HTML::Entities
22             for (0..255) {
23             $URI_ESCAPES{ chr $_ } = sprintf('%%%02X', $_);
24             $HTML_ENTITIES{ chr $_ } = "&#$_;";
25             }
26              
27             sub new {
28 14     14 0 214 my $class = shift;
29 14         29 my %attr = @_;
30              
31 14   66     58 $class = ref $class || $class;
32 14         36 bless my $self = {}, $class;
33              
34 14         43 $self->initialize;
35 14         36 @$self{ keys %attr } = values %attr;
36              
37 14         30 $self;
38             }
39              
40             sub initialize {
41 14     14 0 19 my $self = shift;
42              
43 14         52 $self->{template_path} = $CWD;
44 14         131 $self->{'header'} = new HTTP::Headers;
45 14         123 $self->{buffer} = 0;
46 14         23 $self->{content} = '';
47 14         23 $self->{cache_module} = 'Text::Forge::MemCache';
48              
49 14         48 $self->{autoload} = {
50             template_path => 1,
51             'header' => 1,
52             buffer => 1,
53             content => 1,
54             cache_module => 1,
55             };
56              
57 14         106 $self->{ops} = {
58             '%' => \&Text::Forge::_op_perl,
59             ' ' => \&Text::Forge::_op_perl,
60             "\n" => \&Text::Forge::_op_perl,
61             '$' => \&Text::Forge::_op_interp,
62             '=' => \&Text::Forge::_op_html_encode,
63             '?' => \&Text::Forge::_op_uri_escape,
64             };
65             }
66              
67 0     0 0 0 sub version { $VERSION }
68              
69             # Perform a deep copy of ourself.
70             # Note that this only works for one level.
71             sub clone {
72 9     9 0 47 my $self = shift;
73 9         35 my %attr = @_;
74              
75 9         21 my $clone = $self->new;
76 9         865 $clone->{'header'} = $self->{'header'}->clone;
77            
78 9         67 foreach my $key (keys %$self) {
79 63 100       119 next if $key eq 'header';
80 54         73 my $ref = ref $self->{ $key };
81 54 100       96 $clone->{ $key } = { %{ $self->{ $key } } }, next if $ref eq 'HASH';
  18         116  
82 36 50       64 $clone->{ $key } = [ @{ $self->{ $key } } ], next if $ref eq 'ARRAY';
  0         0  
83 36         62 $clone->{ $key } = $self->{ $key };
84             }
85            
86 9         56 @$clone{ keys %attr } = values %attr;
87 9         31 $clone;
88             }
89              
90             # Construct a unique package name based on the template path.
91             # Based on Apache::Registry
92             sub _package {
93 9     9   14 my $self = shift;
94 9         17 my $path = shift;
95              
96 9         61 $path =~ s/([^A-Za-z0-9\/])/sprintf('_%02x', ord $1)/eg;
  54         201  
97 9         102 $path =~ s{ (/+)(\d?) }
98 45 50       216 { '::' . (length $2 ? sprintf('_%02x', ord $2) : '') }egx;
99 9         38 return "Text::Forge::Template$path";
100             }
101              
102             # Parse the template and write the code
103             # Based on Apache::Cachet
104             sub _parse {
105 9     9   14 my $self = shift;
106 9         15 my $doc = shift;
107 9         12 my($pre, $post, $string, $op, @code);
108              
109 9 100       66 $pre = $1 if $doc =~ s#^(.*)<\s*FORGE\s*>##si;
110 9 50       33 $post = $1 if $doc =~ s#<\s*/\s*FORGE\s*>(.*)$##si;
111              
112 9         68 my @tokens = split /<%(.)(.*?)%>/s, $doc;
113 9         29 while(@tokens) {
114 23         27 $string = shift @tokens;
115 23 100       45 if (length $string) {
116             # Strip whitespace from end of tag -- we place the whitespace in
117             # a code segment, so errors still report the proper line numbers
118 19 100 100     145 if ((@code or defined $pre) and $string =~ /^([ \t\r\f]*\n)(.*)$/s) {
      100        
119 10         22 $string = $2;
120 10         29 push(@code, $self->{ops}{'%'}->( $1 ));
121             }
122 19 0       49 $string =~ s/((?:\\.)|(?:\|))/$1 eq '|' ? '\\|' : $1/eg;
  0         0  
123 19 100       58 push(@code, qq( print qq|$string|; )) if length $string;
124             }
125 23 100       46 last unless @tokens;
126 14         59 ($op, $string) = (shift @tokens, shift @tokens);
127 14 50       39 exists $self->{ops}{ $op } or croak "unknown op '$op'";
128 14         33 push @code, $self->{ops}{ $op }->( $string );
129             }
130              
131 9         69 return( join('', @code), $pre, $post );
132             }
133              
134             sub _build_code {
135 9     9   12 my $self = shift;
136 9         18 my($path, $package, $code, $pre, $post) = @_;
137              
138 9         36 local $^W = 0;
139              
140 9         73 join '',
141             "sub {\n",
142             " no strict;\n",
143             " package $package;\n",
144             " my \$forge = shift;\n\n",
145             "# line 1 $path\n",
146             "$pre ; \$forge->_pre_template; ",
147             $code,
148             "$post ; }; ";
149             }
150              
151             # Last chance for initialization prior to main template body
152             sub _pre_template {
153 10     10   15 my $self = shift;
154              
155 10 50 33     82 return if $self->{_header_sent} or $self->{_tie_obj};
156 0 0       0 return $self->_tie_stdout if $self->{buffer};
157              
158 0         0 my $header = $self->{'header'}->as_string;
159 0 0       0 print "$header\n" if $header;
160 0         0 $self->{_header_sent} = 1;
161             }
162              
163             # This is run after we've finished with the entire template
164             sub _post_template {
165 9     9   12 my $self = shift;
166              
167 9 50       30 return unless $self->{_tie_obj};
168              
169 9         11 $self->{content} = join '', @{ $self->{_tie_obj } };
  9         30  
170 9         25 $self->_untie_stdout;
171             }
172              
173             sub _tie_stdout {
174 9     9   15 my $self = shift;
175              
176 9 50       29 return if $self->{_tie_obj}; # Already tied?
177 9         23 $self->{_old_tie} = tied *STDOUT;
178 9 50       50 $self->{_tie_obj} = tie *STDOUT, 'Text::Forge'
179             or croak "unable to tie STDOUT: $!";
180             }
181              
182             sub _untie_stdout {
183 9     9   14 my $self = shift;
184              
185 9 50       22 return unless $self->{_tie_obj};
186 9         18 undef $self->{_tie_obj};
187 9         59 untie *STDOUT;
188 9 50       31 tie *STDOUT, ref $self->{_old_tie} if $self->{_old_tie};
189 9         17 undef $self->{_old_tie};
190             }
191              
192             # Eval the code and cache it for next time.
193             sub compile {
194 9     9 0 11 my $self = shift;
195 9         20 my @paths = @_;
196 9         9 my($sub, @subs);
197 9         30 local $/;
198              
199 9         18 foreach my $path (@paths) {
200 9 50       42 $path = "$self->{template_path}/$path" unless $path =~ m#^/#;
201 9 50       33 File::PathConvert::regularize( $path ) # 19% faster when skipped
202             unless $self->{'cache_module'}->is_cached( $path );
203              
204 9         222 my $fh = Symbol::gensym;
205 9 50       612 open $fh, $path or croak "unable to read '$path': $!";
206 9         144 my $template = <$fh>;
207 9 50       86 close $fh or croak "error closing '$path': $!";
208 9         42 my $package = $self->_package( $path );
209 9         37 my $code = $self->_build_code($path, $package, $self->_parse( $template ));
210 9 50       30 print STDERR "\n--- Start Code $path ---\n\n$code",
211             "\n\n--- End Code $path ---\n\n" if $Debug;
212             # Eval doesn't catch warnings, so we set up our own warn handler
213             {
214 9         11 my $warning = '';
  9         13  
215 9     0   78 local $SIG{__WARN__} = sub { $warning .= shift() };
  0         0  
216 5     5   38 $sub = eval $code;
  5     3   14  
  5     1   2508  
  3         18  
  3         4  
  3         1119  
  1         6  
  1         2  
  1         733  
  9         776  
217 9 50 33     108 croak "$warning$@" if $warning or $@;
218             }
219 9         113 $self->{'cache_module'}->store( $path, $sub );
220 9         47 push @subs, $sub;
221             }
222              
223 9 50       80 return (wantarray ? @subs : $subs[0]);
224             }
225              
226             # This should only be called from within a template
227             sub include {
228 10     10 0 18 my $self = shift;
229 10         15 my $path = shift;
230              
231 10 50       29 $path or croak 'no path specified';
232            
233 10 50       61 $path = "$self->{template_path}/$path" unless $path =~ m#^/#;
234 10 100       73 File::PathConvert::regularize( $path ) # 19% faster when skipped
235             unless $self->{'cache_module'}->is_cached( $path );
236              
237 10   66     894 my $sub = $self->{'cache_module'}->fetch( $path ) ||
238             $self->compile( $path );
239              
240 10         297 return $sub->($self, @_);
241             }
242              
243             # Process the template but don't generate any output
244             sub generate {
245 9     9 0 209 my $self = shift;
246 9         15 my $path = shift;
247              
248 9 50       35 croak 'cannot call generate() or send() from within template'
249             if $self->{_in_template};
250              
251 9         39 my $clone = $self->clone( _in_template => 1,
252             _header_sent => 0,
253             content => '' );
254 9         37 $clone->_tie_stdout;
255 9         33 $clone->include( $path, @_ );
256 9         46 $clone->_post_template;
257              
258 9         70 return $clone;
259             }
260              
261             # Just like generate() but we output the result
262             sub send {
263 0     0 0 0 my $self = shift;
264 0         0 my $path = shift;
265              
266 0 0       0 croak 'cannot call generate() or send() from within template'
267             if $self->{_in_template};
268              
269 0         0 my $clone = $self->clone( _in_template => 1,
270             _header_sent => 0,
271             content => '' );
272 0         0 $clone->include( $path, @_ );
273 0         0 $clone->_post_template;
274              
275             # Send the document if we haven't already
276 0 0       0 unless ($clone->{_header_sent}) {
277 0         0 my $header = $clone->{'header'}->as_string;
278 0 0       0 print "$header\n" if $header;
279 0         0 print $clone->{content};
280             }
281              
282 0         0 return $clone;
283             }
284              
285             sub op_handler {
286 0     0 0 0 my $self = shift;
287              
288 0 0       0 return keys %{ $self->{ops} } unless @_;
  0         0  
289              
290 0         0 my $op = shift;
291 0 0       0 return $self->{ops}{ $op } unless @_;
292              
293 0         0 my $sub = shift;
294 0 0       0 ref $sub eq 'CODE' or croak 'op handler must be code reference';
295 0         0 $self->{ops}{ $op } = $sub;
296             }
297              
298             sub header {
299 2     2 0 6 my $self = shift;
300              
301 2 50       6 return $self->{'header'} unless @_;
302 2 50       5 if (ref $_[0] eq 'ARRAY') {
303 0         0 $self->{'header'} = new HTTP::Headers;
304 0 0       0 return (wantarray ? () : undef);
305             }
306 2         7 $self->{'header'}->header( @_ );
307             }
308              
309             sub as_string {
310 4     4 0 20 my $self = shift;
311              
312 4         14 my $header = $self->{'header'}->as_string;
313 4 50       155 $header .= "\n" if $header;
314 4         11 return "$header$self->{content}";
315             }
316              
317 17     17   31 sub _op_perl { my $data = shift; qq( $data; ) }
  17         43  
318 0     0   0 sub _op_interp { my $data = shift; qq( print $data; ) }
  0         0  
319              
320             # Inline encoding to remove overhead of function call
321             sub _op_html_encode {
322 4     4   6 my $data = shift;
323              
324 4         19 return qq( print map { my \$e = \$_; \$e =~ s{([^\\n\\t !#\$%'-;=?-~])}{\$Text::Forge::HTML_ENTITIES{ \$1 }}gx; \$e; } $data; );
325             }
326              
327             # Inline escaping to remove overhead of function call
328             sub _op_uri_escape {
329 3     3   4 my $data = shift;
330              
331 3         11 return qq( print map { my \$e = \$_; \$e =~ s/([^A-Za-z0-9])/\$Text::Forge::URI_ESCAPES{ \$1 }/g; \$e; } $data; );
332             }
333              
334             # Convenience methods
335              
336             sub uri_escape {
337 0     0 0 0 my $self = shift;
338 0         0 my @values = @_;
339 0         0 local $_;
340              
341 0         0 foreach (@values) {
342 0         0 s/([^A-Za-z0-9])/$Text::Forge::URI_ESCAPES{ $1 }/g;
343             }
344 0 0       0 return (wantarray ? @values : $values[0]);
345             }
346              
347             sub encode_entities {
348 0     0 0 0 my $self = shift;
349 0         0 my @values = @_;
350 0         0 local $_;
351              
352 0         0 foreach (@values) {
353 0         0 s/([^\n\t !\#\$%\'-;=?-~])/$Text::Forge::HTML_ENTITIES{ $1 }/g;
354             }
355 0 0       0 return (wantarray ? @values : $values[0]);
356             }
357              
358             sub AUTOLOAD {
359 5     5   33 my $self = shift;
360 5   33     19 my $type = ref($self) || croak "autoload: $self is not an object";
361 5         10 my $name = $AUTOLOAD;
362              
363 5         32 $name =~ s/.*://;
364 5 50       17 return if $name eq 'DESTROY';
365 5 50       34 croak "unknown autoload name '$name'" unless exists $self->{autoload}{$name};
366 5 50       27 return (@_ ? $self->{$name} = shift : $self->{$name});
367             }
368              
369             # The next few routines are used for tying stdout when buffering is on.
370              
371             sub TIEHANDLE {
372 9     9   15 my $class = shift;
373              
374 9         60 bless [], $class;
375             }
376              
377 0     0   0 sub WRITE { croak 'write not implemented!' }
378              
379             sub PRINT {
380 25     25   31 my $self = shift;
381              
382 25         82 push @$self, @_;
383             }
384            
385             sub PRINTF {
386 0     0   0 my $self = shift;
387 0         0 my $fmt = shift;
388              
389 0         0 push @$self, sprintf($fmt, @_);
390             }
391              
392             sub DESTROY {
393 23     23   229 my $self = shift;
394              
395 23 50 33     328 $self->_untie_stdout if $self->{_tie_obj} and not $self->{_in_template};
396             }
397              
398             1;
399              
400             __END__