File Coverage

blib/lib/HTML/Tmojo.pm
Criterion Covered Total %
statement 287 443 64.7
branch 134 236 56.7
condition 19 27 70.3
subroutine 18 22 81.8
pod 0 12 0.0
total 458 740 61.8


line stmt bran cond sub pod time code
1             ###########################################################################
2             # Copyright 2003, 2004 Lab-01 LLC
3             #
4             # Licensed under the Apache License, Version 2.0 (the "License");
5             # you may not use this file except in compliance with the License.
6             # You may obtain a copy of the License at
7             #
8             # http://www.apache.org/licenses/LICENSE-2.0
9             #
10             # Unless required by applicable law or agreed to in writing, software
11             # distributed under the License is distributed on an "AS IS" BASIS,
12             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13             # See the License for the specific language governing permissions and
14             # limitations under the License.
15             #
16             # Tmojo(tm) is a trademark of Lab-01 LLC.
17             ###########################################################################
18              
19             package HTML::Tmojo;
20              
21             our $VERSION = '0.300';
22              
23             =head1 NAME
24              
25             HTML::Tmojo - Dynamic Text Generation Engine
26              
27             =head1 SYNOPSIS
28              
29             my $tmojo = HTML::Tmojo->new(
30             template_dir => '/location/of/templates',
31             cache_dir => '/place/to/save/compiled/templates',
32             );
33            
34             my $result = $tmojo->call('my_template.tmojo', arg1 => 1, arg2 => 3);
35            
36             # HONESTLY, THIS SYNOPSIS DOESN'T COVER NEARLY ENOUGH.
37             # GO READ TMOJO IN A NUTSHELL
38              
39             =head1 ABSTRACT
40              
41             Tmojo is used for generating dynamic text documents.
42             While it is particularly suited to generating HTML
43             and XML documents, it can be used effectively to
44             produce any text output, including dynamically
45             generated source code.
46              
47             =head1 AUTHOR
48              
49             Will Conant
50              
51             =cut
52              
53 5     5   105852 use strict;
  5         11  
  5         179  
54 5     5   4991 use Data::Dumper;
  5         50669  
  5         407  
55 5     5   4171 use Symbol qw(delete_package);
  5         4528  
  5         303  
56              
57 5     5   2425 use HTML::Tmojo::TemplateLoader;
  5         11  
  5         1248  
58              
59             our %memory_cache;
60              
61             sub new {
62 13     13 0 44625 my ($class, %args) = @_;
63              
64 13 100       241 if (defined $args{template_dir}) {
    50          
65 5         376 $args{template_loader} = HTML::Tmojo::TemplateLoader->new($args{template_dir}, $args{tmojo_lite});
66 5         53 delete $args{template_dir};
67             }
68             elsif (not defined $args{template_loader}) {
69 0         0 $args{template_loader} = HTML::Tmojo::TemplateLoader->new($ENV{TMOJO_TEMPLATE_DIR}, $args{tmojo_lite});
70             }
71            
72             %args = (
73 13         223 cache_dir => $ENV{TMOJO_CACHE_DIR},
74             context_path => '',
75            
76             %args,
77             );
78            
79 13         59 $args{cache_dir} =~ s/\/$//;
80            
81 13         85 my $self = {
82             %args
83             };
84            
85 13         105 return bless $self, $class;
86             }
87              
88             sub call {
89 8     8 0 6422 my ($self, $template_id, %args) = @_;
90 8         80 return $self->call_with_container($template_id, undef, %args);
91             }
92              
93             sub call_with_container {
94 8     8 0 33 my ($self, $template_id, $container_override_id, %args) = @_;
95            
96 8         50 my $result = eval {
97            
98 8         83 my $current_package = $self->get_template_class($template_id);
99 8         80 my $current_template = $current_package->new(\%args);
100            
101             # WE HAVE TO KEEP TRACK OF WHICH CONTAINERS HAVE BEEN USED,
102             # SO THAT USERS CAN'T CREATE AN INFINITE CONTAINER LOOP
103 8         293 my %used_containers = (
104             $self->normalize_template_id($template_id) => 1,
105             );
106            
107 8         20 for (;;) {
108 5     5   25 no strict 'refs';
  5         7  
  5         28433  
109            
110 9         36 my $contextual_tmojo = ${$current_package . '::Tmojo'};
  9         55  
111            
112 9         18 my $container_id;
113 9 50       89 if (defined $container_override_id) {
114 0         0 $container_id = $container_override_id;
115 0         0 $container_override_id = undef;
116             }
117             else {
118 9         18 $container_id = ${$current_package . '::TMOJO_CONTAINER'};
  9         58  
119             }
120            
121 9 100       31 if (defined $container_id) {
122             # NORMALIZE THE CONTAINER ID FOR GOOD MEASURE
123 1         6 $container_id = $contextual_tmojo->normalize_template_id($container_id);
124            
125             # CHECK TO MAKE SURE THAT THE CONTAINER HASN'T ALREADY BEEN USED
126 1 50       5 if (defined $used_containers{$container_id}) {
127 0         0 die "circular container reference, $container_id already used (this will cause an infinite loop)";
128             }
129            
130             # PUT IT IN THE USED LIST
131 1         4 $used_containers{$container_id} = 1;
132            
133             # MOVE ON UP
134 1         4 $current_package = $contextual_tmojo->get_template_class($container_id);
135 1         11 $current_template = $current_package->new(\%args, $current_template);
136             }
137             else {
138 8         38 return $current_template->main();
139             }
140             }
141            
142             };
143 8 50       388 if ($@) {
144 0         0 $self->report_error($@);
145             }
146            
147 8         129 return $result;
148             }
149              
150             sub prepare {
151 0     0 0 0 my ($self, $template_id, %args) = @_;
152            
153 0         0 my $package = $self->get_template_class($template_id);
154 0         0 my $template = $package->new(\%args);
155            
156 0         0 return $template;
157             }
158              
159             sub template_exists {
160 0     0 0 0 my ($self, $template_id) = @_;
161            
162 0         0 $template_id = $self->normalize_template_id($template_id);
163 0         0 return $self->{template_loader}->template_exists($template_id);
164             }
165              
166             sub report_error {
167 0     0 0 0 my ($self, $error) = @_;
168            
169 0         0 my $err = (split(/\n/, $error))[0];
170 0 0       0 if ($err =~ /at ([^\s]+) line\s+(\d+)/) {
171 0         0 my $file_name = $1;
172 0         0 my $line_number = $2;
173            
174 0         0 my $template_id;
175            
176 0         0 open FH, "$file_name.lines";
177 0         0 local $/ = "\n"; # THIS CAN GET EXTRA SCREWED UP IN MOD_PERL
178            
179 0         0 my $cur_line = 1;
180 0         0 while (my $line = ) {
181 0 0       0 if ($line =~ /^###TMOJO_TEMPLATE_ID: (.+)$/) {
182 0         0 $template_id = $1;
183 0         0 chomp $template_id;
184             }
185            
186 0 0       0 if ($cur_line == $line_number) {
187 0 0       0 if ($line =~ /###TMOJO_LINE: (\d+)$/) {
188 0         0 die "Error at $template_id line $1.\n$@";
189             }
190             }
191            
192 0         0 $cur_line += 1;
193             }
194 0         0 close FH;
195             }
196            
197 0         0 die $error;
198             }
199              
200             sub parse_template {
201 8     8 0 19 my ($source) = @_;
202            
203 8         14 my @parsed;
204            
205 8         34 my $tag_open = "<:";
206 8         25 my $tag_close = ":>";
207 8         16 my $tag_line = ":";
208            
209 8         15 my $tag_open_r;
210             my $tag_close_r;
211 0         0 my $tag_line_r;
212            
213             my $make_regexes = sub {
214 12     12   22 $tag_open_r = $tag_open;
215 12         22 $tag_close_r = $tag_close;
216 12         19 $tag_line_r = $tag_line;
217            
218 12         55 $tag_open_r =~ s/([\[\]\{\}\(\)\$\@\^\\\|\?\*\+])/\\$1/g;
219 12         44 $tag_close_r =~ s/([\[\]\{\}\(\)\$\@\^\\\|\?\*\+])/\\$1/g;
220 12         31 $tag_line_r =~ s/([\[\]\{\}\(\)\$\@\^\\\|\?\*\+])/\\$1/g;
221 8         125 };
222            
223             my $count_newlines = sub {
224 73     73   108 my $count = 0;
225 73         96 my $pos = 0;
226            
227 73         192 while ($pos > -1) {
228 127         216 $pos = index($_[0], "\n", $pos);
229 127 100       474 if ($pos > -1) {
230 54         72 $pos += 1;
231 54         125 $count += 1;
232             }
233             }
234            
235 73         152 return $count;
236 8         63 };
237            
238 8         29 $make_regexes->();
239            
240 8         29 my $keywords = "GLOBAL|INIT|METHOD|PERL|MERGE|CAPTURE|FILTER|REGEX|NOP|TAG_STYLE";
241 8         411 my %crush_defaults = (
242             'GLOBAL' => [0, 0],
243             '/GLOBAL' => [0, 2],
244            
245             'INIT' => [0, 0],
246             '/INIT' => [0, 2],
247            
248             'METHOD' => [0, 2],
249             '/METHOD' => [2, 2],
250            
251             'PERL' => [1, 0],
252             '/PERL' => [0, 0],
253            
254             'MERGE' => [0, 0],
255            
256             'CAPTURE' => [1, 2],
257             '/CAPTURE' => [2, 0],
258            
259             'FILTER' => [0, 0],
260             '/FILTER' => [0, 0],
261            
262             'REGEX' => [0, 0],
263             '/REGEX' => [0, 0],
264            
265             'TAG_STYLE' => [1, 0],
266            
267             'NOP' => [0, 0],
268             );
269            
270 8         23 my $current_line = 1;
271              
272 8         35 while ($source ne '') {
273            
274             # SNAG THE NEXT TAG
275             # -------------------
276            
277 45         70 my $found_tag = 0;
278 45         67 my $tag_notation;
279             my $pre_tag_text;
280              
281 45 100       129 if (scalar(@parsed) == 0) {
282 8 100       426 if ($source =~ s/^([ \t]*)$tag_line_r//s) {
283 1         2 $found_tag = 1;
284 1         2 $tag_notation = 'line';
285 1         4 $pre_tag_text = $1;
286             }
287             }
288            
289 45 100       130 unless ($found_tag == 1) {
290 44 100       722 if ($source =~ s/^(.*?)($tag_open_r|(\n[ \t]*)$tag_line_r)//s) {
291 38         64 $found_tag = 1;
292            
293             # DETERMINE IF THIS IS A LINE OR INLINE TAG
294 38 100       168 if ($2 eq $tag_open) {
295 31         75 $tag_notation = 'inline';
296             }
297             else {
298 7         12 $tag_notation = 'line';
299             }
300            
301             # DETERMINE THE PRE TAG TEXT
302 38         110 $pre_tag_text = $1;
303 38 100       179 if ($tag_notation eq 'line') {
304 7         23 $pre_tag_text .= $3;
305             }
306             }
307             }
308            
309 45 100       129 if ($found_tag == 1) {
    50          
310            
311 39 100       102 if ($pre_tag_text ne '') {
312             # PUSH PLAIN TEXT ONTO THE PARSED RESULT
313 34         251 push @parsed, { type => 'TEXT', text => $pre_tag_text, source => $pre_tag_text, crush_before => 0, crush_after => 0, start_line => $current_line };
314            
315             # COUNT THE NUMBER OF NEWLINES
316 34         87 $current_line += $count_newlines->($pre_tag_text);
317             }
318            
319             # GRAB THE REST OF THE TAG
320 39         55 my $tag_source;
321             my $tag_inside;
322            
323 39 100       102 if ($tag_notation eq 'inline') {
324 31         49 $tag_source = $tag_line;
325            
326 31 50       1062 if ($source =~ s/^(.*?)$tag_close_r//s) {
327 31         80 $tag_inside = $1;
328 31         106 $tag_source .= "$1$tag_close";
329             }
330             else {
331 0         0 die "expected '$tag_close'";
332             }
333             }
334             else {
335 8         11 $tag_source = $tag_open;
336            
337             # GOBBLE UP THE REST OF THE LINE
338 8         38 $source =~ s/^([^\n]*)//;
339 8         16 $tag_inside = $1;
340 8         18 $tag_source .= $1;
341             }
342            
343             # NOTCH UP THE LINES
344 39         130 $current_line += $count_newlines->($tag_source);
345            
346             # PARSE THE TAG INSIDES
347            
348 39         185 my %tag = (
349             source => $tag_source,
350             start_line => $current_line,
351             );
352            
353             # LOOK FOR WHITESPACE CRUSHERS
354            
355 39 100       119 if ($tag_notation eq 'inline') {
356 31 100       168 if ($tag_inside =~ s/^--//) {
    50          
    50          
357 2         4 $tag{crush_before} = 2;
358             }
359             elsif ($tag_inside =~ s/^-//) {
360 0         0 $tag{crush_before} = 1;
361             }
362             elsif ($tag_inside =~ s/^\+//) {
363 0         0 $tag{crush_before} = 0;
364             }
365            
366 31 100       201 if ($tag_inside =~ s/--$//) {
    100          
    50          
367 1         3 $tag{crush_after} = 2;
368             }
369             elsif ($tag_inside =~ s/-$//) {
370 1         108 $tag{crush_after} = 1;
371             }
372             elsif ($tag_inside =~ s/\+$//) {
373 0         0 $tag{crush_after} = 0;
374             }
375             }
376            
377             # FIGURE OUT THE TAG TYPE
378            
379 39 50       1047 if ($tag_inside =~ /^\s*$/) {
    100          
    100          
380 0         0 $tag{type} = 'NOP';
381             }
382             elsif ($tag_inside =~ s/^\s*(\/?(?:$keywords))\s+//) {
383 18         85 $tag{type} = $1;
384             }
385             elsif ($tag_notation eq 'inline') {
386             # USE A LITTLE MAGIC TO SEE IF WE'VE GOT A STATEMENT OR AN EXPRESSION
387 13 50       122 if ($tag_inside =~ /^\s*(if|unless|while|until|for|foreach)\s+/) {
    50          
    50          
388             # THIS LOOKS LIKE A PERL STATEMENT
389 0         0 $tag{type} = 'PERL';
390             }
391             elsif ($tag_inside =~ /^\s*\}?\s*(else|elsif|continue)\s+/) {
392             # THIS LOOKS LIKE A PERL STATEMENT
393 0         0 $tag{type} = 'PERL';
394             }
395             elsif ($tag_inside =~ /^\s*\}\s*$/) {
396             # THIS LOOKS LIKE A PERL STATEMENT
397 0         0 $tag{type} = 'PERL';
398             }
399             else {
400             # MUST BE A PERL EXPRESSION
401 13         44 $tag{type} = 'MERGE';
402             }
403             }
404             else {
405 8         18 $tag{type} = 'PERL';
406             }
407            
408             # PUT WHAT'S LEFT IN THE TAG TEXT
409            
410 39         268 $tag_inside =~ s/(^\s+|\s+$)//g;
411 39         124 $tag{text} = $tag_inside;
412            
413             # SET DEFAULT CRUSHING
414            
415 39 100       111 if (not defined $tag{crush_before}) {
416 37         172 $tag{crush_before} = $crush_defaults{$tag{type}}[0];
417             }
418            
419 39 100       125 if (not defined $tag{crush_after}) {
420 37         105 $tag{crush_after} = $crush_defaults{$tag{type}}[1];
421             }
422            
423            
424             # HANDLE FIRST-PASS TAGS
425             # ----------------------
426 39 100       113 if ($tag{type} eq 'TAG_STYLE') {
427 4 100       12 if ($tag{text} eq 'default') {
428 1         4 ($tag_open, $tag_close, $tag_line) = ('<:', ':>', ':');
429             }
430             else {
431 3         14 ($tag_open, $tag_close, $tag_line) = split /\s+/, $tag{text};
432             }
433            
434 4 50       13 if ($tag_open eq '') {
435 0         0 die "invalid open tag marker";
436             }
437            
438 4 50       11 if ($tag_close eq '') {
439 0         0 die "invalid close tag marker";
440             }
441            
442 4 50       11 if ($tag_line eq '') {
443 0         0 die "invalid line tag marker";
444             }
445            
446 4 50 33     30 if ($tag_line eq $tag_open or $tag_line eq $tag_close) {
447 0         0 die "line tag marker must not be the same as either the open tag marker or close tag marker";
448             }
449            
450 4         19 $make_regexes->();
451             }
452            
453             # PUSH THE TAG ONTO THE RESULT
454            
455 39         366 push @parsed, \%tag;
456             }
457             elsif ($source ne '') {
458 6         85 push @parsed, { type => 'TEXT', text => $source, source => $source, crush_before => 0, crush_after => 0, start_line => $current_line };
459 6         43 $source = '';
460             }
461             }
462            
463             # RUN THROUGH AGAIN AND CRUSH WHITESPACE
464 8         44 for (my $i = 0; $i < scalar(@parsed); $i++) {
465 79 100 100     778 if ($parsed[$i]{crush_before} == 1 and $i > 0 and $parsed[$i-1]{type} eq 'TEXT') {
    100 66        
      66        
      66        
466 11         76 $parsed[$i-1]{text} =~ s/\n?[ \t]*$//;
467             }
468             elsif ($parsed[$i]{crush_before} == 2 and $i > 0 and $parsed[$i-1]{type} eq 'TEXT') {
469 6         43 $parsed[$i-1]{text} =~ s/\s+$//;
470             }
471            
472 79 100 66     763 if ($parsed[$i]{crush_after} == 1 and $i < (scalar(@parsed)-1) and $parsed[$i+1]{type} eq 'TEXT') {
    100 66        
      100        
      66        
473 1         7 $parsed[$i+1]{text} =~ s/^[ \t]*\n?//;
474             }
475             elsif ($parsed[$i]{crush_after} == 2 and $i < (scalar(@parsed)-1) and $parsed[$i+1]{type} eq 'TEXT') {
476 9         88 $parsed[$i+1]{text} =~ s/^\s+//;
477             }
478             }
479            
480             # AND WE'RE DONE
481 8         151 return \@parsed;
482             }
483              
484             sub compile_template {
485 8     8 0 21 my ($source, $template_id, $package_name) = @_;
486            
487             # ADJUST FOR SOURCE LINES
488 8 50       47 if (ref($source) eq 'ARRAY') {
489 8         122 $source = join "", @$source;
490             }
491            
492             # PARSE THE SOURCE INTO TAGS
493 8         116 my $tags = parse_template($source);
494            
495             # INITIALIZE OUR PARSE VARIABLES
496 8         25 my $global_section = '';
497 8         71 my $init_section = '';
498            
499 8         34 my %methods = (
500             main => '',
501             );
502            
503 8         23 my $cur_method = 'main';
504            
505 8         14 my @stack;
506             my @stack_details;
507 0         0 my @stack_lines;
508            
509             # DEFINE A USEFUL LITTLE FUNCTION
510             my $format_perl = sub {
511 2     2   5 my ($source, $start_line) = @_;
512            
513 2         11 my @lines = split /\n/, $source;
514            
515 2         3 my $result;
516 2         9 my $cur_line = $start_line;
517 2         7 foreach my $line (@lines) {
518 4         21 $result .= "$line###TMOJO_LINE: $cur_line\n";
519 4         9 $cur_line += 1;
520             }
521            
522 2         14 return $result;
523 8         86 };
524            
525             # PARSE ALL OF THE TAGS
526 8         42 while (my $tag = shift @$tags) {
527            
528             # TEXT TAG
529             # ---------------------------------
530            
531 75 100       456 if ($tag->{type} eq 'TEXT') {
    100          
    50          
    100          
    100          
    100          
    100          
    50          
    50          
    50          
    50          
    50          
    50          
532 38         369 my $dumper = Data::Dumper->new([$tag->{text}]);
533 38         1860 $dumper->Useqq(1);
534 38         378 $dumper->Indent(0);
535 38         440 $dumper->Terse(1);
536 38         285 my $literal = $dumper->Dump();
537            
538 38         7404 $methods{$cur_method} .= "\t\$Result .= $literal;\n";
539             }
540            
541             # GLOBAL TAG
542             # ---------------------------------
543            
544             elsif ($tag->{type} eq 'GLOBAL') {
545            
546 1 50       4 if ($cur_method ne 'main') {
547 0         0 die "cannot declare METHOD here";
548             }
549            
550 1 50       4 if ($global_section ne '') {
551 0         0 die "attempting to redefine GLOBAL section";
552             }
553            
554 1         6 my $source = '';
555 1         2 my $start_line;
556            
557 1         5 while (my $tag = shift @$tags) {
558 2 50       6 if (not defined $tag) {
559 0         0 die "missing /GLOBAL tag";
560             }
561            
562 2 100       8 if ($tag->{type} eq '/GLOBAL') {
    50          
563 1         4 last;
564             }
565             elsif ($tag->{type} ne 'TEXT') {
566 0         0 die "non-text tag in GLOBAL section in '$template_id' starting at line $tag->{start_line}";
567             }
568             else {
569 1 50       3 if (not defined $start_line) {
570 1         2 $start_line = $tag->{start_line};
571             }
572 1         8 $source .= $tag->{source};
573             }
574             }
575            
576 1         17 $global_section .= $format_perl->($source, $start_line);
577             }
578            
579             # INIT TAG
580             # ---------------------------------
581            
582             elsif ($tag->{type} eq 'INIT') {
583            
584 0 0       0 if ($cur_method ne 'main') {
585 0         0 die "cannot declare METHOD here";
586             }
587            
588 0 0       0 if ($init_section ne '') {
589 0         0 die "attempting to redefine INIT section";
590             }
591            
592 0         0 my $source = '';
593 0         0 my $start_line;
594            
595 0         0 while (my $tag = shift @$tags) {
596 0 0       0 if (not defined $tag) {
597 0         0 die "missing /INIT tag";
598             }
599            
600 0 0       0 if ($tag->{type} eq '/INIT') {
    0          
601 0         0 last;
602             }
603             elsif ($tag->{type} ne 'TEXT') {
604 0         0 die "non-text tag in INIT section in '$template_id' starting at line $tag->{start_line}";
605             }
606             else {
607 0 0       0 if (not defined $start_line) {
608 0         0 $start_line = $tag->{start_line};
609             }
610 0         0 $source .= $tag->{source};
611             }
612             }
613            
614 0         0 $init_section .= $format_perl->($source, $start_line);
615             }
616            
617             # PERL TAG
618             # ---------------------------------
619            
620             elsif ($tag->{type} eq 'PERL') {
621            
622 11 100       24 if ($tag->{text} ne '') {
623 10         39 my @lines = split /\n/, $tag->{text};
624            
625 10         19 my $cur_line = $tag->{start_line};
626 10         31 while ($_ = shift @lines) {
627 11         31 $methods{$cur_method} .= "$_###TMOJO_LINE: $cur_line\n";
628 11         65 $cur_line += 1;
629             }
630             }
631             else {
632 1         8 my $source = '';
633 1         2 my $start_line;
634            
635 1         4 while (my $tag = shift @$tags) {
636 2 50       5 if (not defined $tag) {
637 0         0 die "missing /PERL tag";
638             }
639            
640 2 100       7 if ($tag->{type} eq '/PERL') {
    50          
641 1         3 last;
642             }
643             elsif ($tag->{type} ne 'TEXT') {
644 0         0 die "non-text tag in PERL section in '$template_id' starting at line $tag->{start_line}";
645             }
646             else {
647 1 50       4 if (not defined $start_line) {
648 1         1 $start_line = $tag->{start_line};
649             }
650 1         7 $source .= $tag->{source};
651             }
652             }
653            
654 1         38 $methods{$cur_method} .= $format_perl->($source, $start_line);
655             }
656             }
657            
658             # METHOD TAG
659             # ---------------------------------
660            
661             elsif ($tag->{type} eq 'METHOD') {
662            
663 4 50       14 if ($cur_method ne 'main') {
664 0         0 die "cannot declare METHOD here";
665             }
666            
667 4         14 $cur_method = $tag->{text};
668 4 50       41 if ($cur_method !~ /^[a-zA-Z]\w*$/) {
669 0         0 die "illegal method name $cur_method";
670             }
671            
672 4 50       31 if (defined $methods{$cur_method}) {
673 0         0 die "attempting to redefine METHOD $cur_method";
674             }
675             }
676            
677             # /METHOD TAG
678             # ---------------------------------
679            
680             elsif ($tag->{type} eq '/METHOD') {
681            
682 4 50       103 if ($cur_method eq 'main') {
683 0         0 die "cannot end METHOD here";
684             }
685            
686 4         32 $cur_method = 'main';
687             }
688            
689             # MERGE TAG
690             # ---------------------------------
691            
692             elsif ($tag->{type} eq 'MERGE') {
693            
694             # FORMAT THE PERL
695 13         37 $methods{$cur_method} .= "\t\$Result .= (";
696            
697 13         156 my @lines = split /\n/, $tag->{text};
698            
699 13         29 my $cur_line = $tag->{start_line};
700 13         51 while ($_ = shift @lines) {
701 13         98 $methods{$cur_method} .= $_;
702 13 50       40 if (@lines) {
703 0         0 $methods{$cur_method} .= "###TMOJO_LINE: $cur_line\n";
704             }
705             else {
706 13         64 $methods{$cur_method} .= "); ###TMOJO_LINE: $cur_line\n";
707             }
708            
709 13         101 $cur_line += 1;
710             }
711             }
712            
713             # CAPTURE TAG
714             # ---------------------------------
715            
716             elsif ($tag->{type} eq 'CAPTURE') {
717            
718 0         0 push @stack, 'CAPTURE';
719 0         0 push @stack_details, $tag->{text};
720 0         0 push @stack_lines, $tag->{start_line};
721            
722 0         0 $methods{$cur_method} .= "\tpush(\@ResultStack, ''); local \*Result = \\\$ResultStack[-1];\n";
723             }
724            
725             # /CAPTURE TAG
726             # ---------------------------------
727            
728             elsif ($tag->{type} eq '/CAPTURE') {
729            
730 0 0       0 if (pop(@stack) ne 'CAPTURE') {
731 0         0 die "unexpected /CAPTURE tag>";
732             }
733            
734 0         0 my $capture_lvalue = pop @stack_details;
735 0         0 my $capture_line = pop @stack_lines;
736            
737 0         0 $methods{$cur_method} .= "\t$capture_lvalue = pop(\@ResultStack); local \*Result = \\\$ResultStack[-1];###TMOJO_LINE: $capture_line\n";
738             }
739            
740             # FILTER TAG
741             # ---------------------------------
742            
743             elsif ($tag->{type} eq 'FILTER') {
744            
745 0         0 push @stack, 'FILTER';
746 0         0 push @stack_details, $tag->{text};
747 0         0 push @stack_lines, $tag->{start_line};
748            
749 0         0 $methods{$cur_method} .= "\tpush(\@ResultStack, ''); local \*Result = \\\$ResultStack[-1];\n";
750             }
751            
752             # /FILTER TAG
753             # ---------------------------------
754            
755             elsif ($tag->{type} eq '/FILTER') {
756            
757 0 0       0 if (pop(@stack) ne 'FILTER') {
758 0         0 die "unexpected /FILTER tag>";
759             }
760            
761 0         0 my $filter_code = pop @stack_details;
762 0         0 my $filter_line = pop @stack_lines;
763            
764 0         0 $methods{$cur_method} .= "\t\$ResultStack[-2] .= ($filter_code); pop(\@ResultStack); local \*Result = \\\$ResultStack[-1];###TMOJO_LINE: $filter_line\n";
765             }
766            
767             # REGEX TAG
768             # ---------------------------------
769            
770             elsif ($tag->{type} eq 'REGEX') {
771            
772 0         0 push @stack, 'REGEX';
773 0         0 push @stack_details, $tag->{text};
774 0         0 push @stack_lines, $tag->{start_line};
775            
776 0         0 $methods{$cur_method} .= "\tpush(\@ResultStack, ''); local \*Result = \\\$ResultStack[-1];\n";
777             }
778            
779             # /REGEX TAG
780             # ---------------------------------
781            
782             elsif ($tag->{type} eq '/REGEX') {
783            
784 0 0       0 if (pop(@stack) ne 'REGEX') {
785 0         0 die "unexpected /REGEX tag>";
786             }
787            
788 0         0 my $regex = pop @stack_details;
789 0         0 my $regex_line = pop @stack_lines;
790            
791 0         0 $methods{$cur_method} .= "\t\$Result =~ $regex; \$ResultStack[-2] .= \$Result; pop(\@ResultStack); local \*Result = \\\$ResultStack[-1];###TMOJO_LINE: $regex_line\n";
792             }
793             }
794            
795             # MAKE SURE OUR MODE IS COOL
796 8 50       39 if ($cur_method ne 'main') {
797 0         0 die "expected /METHOD tag";
798             }
799            
800 8 50       32 if (@stack) {
801 0         0 die "expected /$stack[-1] tag";
802             }
803            
804             # NOW, WE CONSTRUCT THE ENTIRE PACKAGE
805             # --------------------------------------
806            
807 8         78 my $template_compiled = qq{###TMOJO_TEMPLATE_ID: $template_id
808             package $package_name;
809              
810             use strict;
811              
812             our \$Tmojo;
813              
814             $global_section
815              
816             sub new {
817             my \$Self = {
818             args => \$_[1],
819             next => \$_[2],
820             vars => {},
821             };
822            
823             bless \$Self, \$_[0];
824            
825             # DEFINE THE IMPLICIT VARIABLES
826             my \$Next = \$Self->{next};
827             our \%Args; local \*Args = \$Self->{args};
828             our \%Vars; local \*Vars = \$Self->{vars};
829            
830             # --- BEGIN USER CODE ---
831             $init_section
832             # --- END USER CODE ---
833            
834             # RETURN THE VALUE
835             return \$Self;
836             }
837             };
838            
839 8         71 foreach my $method (keys %methods) {
840 12         102 $template_compiled .= qq{
841             sub $method {
842             my \$Self = shift \@_;
843            
844             # DEFINE THE IMPLICIT VARIABLES
845             my \$Next = \$Self->{next};
846             our \%Args; local \*Args = \$Self->{args};
847             our \%Vars; local \*Vars = \$Self->{vars};
848            
849             my \@ResultStack = ('');
850             our \$Result; local \*Result = \\\$ResultStack[-1];
851            
852            
853             # --- BEGIN USER CODE ---
854             $methods{$method}
855             # --- END USER CODE ---
856            
857             return \$Result;
858             }
859             };
860             }
861            
862 8         38 $template_compiled .= "\n1;\n";
863            
864 8         109 return $template_compiled;
865             }
866              
867              
868              
869              
870              
871             sub compile_lite_template {
872 0     0 0 0 my ($source, $template_id, $package_name) = @_;
873            
874             # ADJUST FOR SOURCE LINES
875 0 0       0 if (ref($source) eq 'ARRAY') {
876 0         0 $source = join "", @$source;
877             }
878            
879             # PARSE THE SOURCE INTO TAGS
880 0         0 my $tags = parse_template($source);
881            
882 0         0 my %methods = (
883             main => '',
884             );
885            
886 0         0 my $cur_method = 'main';
887            
888             # PARSE ALL OF THE TAGS
889 0         0 while (my $tag = shift @$tags) {
890            
891             # TEXT TAG
892             # ---------------------------------
893            
894 0 0       0 if ($tag->{type} eq 'TEXT') {
    0          
    0          
    0          
895 0         0 my $dumper = Data::Dumper->new([$tag->{text}]);
896 0         0 $dumper->Useqq(1);
897 0         0 $dumper->Indent(0);
898 0         0 $dumper->Terse(1);
899 0         0 my $literal = $dumper->Dump();
900            
901 0         0 $methods{$cur_method} .= "\t\$Result .= $literal;\n";
902             }
903            
904             # METHOD TAG
905             # ---------------------------------
906            
907             elsif ($tag->{type} eq 'METHOD') {
908            
909 0 0       0 if ($cur_method ne 'main') {
910 0         0 die "cannot declare METHOD here";
911             }
912            
913 0         0 $cur_method = $tag->{text};
914 0 0       0 if ($cur_method !~ /^[a-zA-Z]\w*$/) {
915 0         0 die "illegal method name $cur_method";
916             }
917            
918 0 0       0 if ($methods{$cur_method} ne '') {
919 0         0 die "attempting to redefine METHOD $cur_method";
920             }
921             }
922            
923             # /METHOD TAG
924             # ---------------------------------
925            
926             elsif ($tag->{type} eq '/METHOD') {
927            
928 0 0       0 if ($cur_method eq 'main') {
929 0         0 die "cannot end METHOD here";
930             }
931            
932 0         0 $cur_method = 'main';
933             }
934            
935             # MERGE TAG
936             # ---------------------------------
937            
938             elsif ($tag->{type} eq 'MERGE') {
939            
940 0 0       0 if ($tag->{text} =~ /^\$([\w\.]+)$/) {
941 0         0 my $lookup = $1;
942 0         0 $lookup =~ s/\.(\w+)/}{$1/g;
943 0         0 $methods{$cur_method} .= "\t\$Result .= \$args->{$lookup};###TMOJO_LINE: $tag->{start_line}\n";
944             }
945             else {
946 0         0 die "malformed merge tag in $template_id on line $tag->{start_line}";
947             }
948             }
949             }
950            
951             # MAKE SURE OUR MODE IS COOL
952 0 0       0 if ($cur_method ne 'main') {
953 0         0 die "expected /METHOD tag";
954             }
955            
956             # NOW, WE CONSTRUCT THE ENTIRE PACKAGE
957             # --------------------------------------
958            
959 0         0 my $template_compiled = qq{###TMOJO_TEMPLATE_ID: $template_id
960             package $package_name;
961              
962             use strict;
963              
964             our \$Tmojo;
965              
966             sub new {
967             my \$Self = {
968             args => \$_[1],
969             };
970            
971             # RETURN THE VALUE
972             return bless \$Self, \$_[0];
973             }
974             };
975            
976 0         0 foreach my $method (keys %methods) {
977 0         0 $template_compiled .= qq{
978             sub $method {
979             my \$Self = shift \@_;
980            
981             my \$args = \$Self->{args};
982             if (\@_) {
983             \$args = { \@_ };
984             }
985            
986             my \$Result = '';
987            
988             # --- BEGIN USER CODE ---
989             $methods{$method}
990             # --- END USER CODE ---
991            
992             return \$Result;
993             }
994             };
995             }
996            
997 0         0 $template_compiled .= "\n1;\n";
998            
999 0         0 return $template_compiled;
1000             }
1001              
1002             sub get_template_class {
1003              
1004 9     9 0 29 my ($self, $template_id, $used_parents) = @_;
1005            
1006             # NORMALIZE THE TEMPLATE_ID
1007 9         99 my $normalized_template_id = $self->normalize_template_id($template_id);
1008            
1009             # GET THE PACKAGE NAME
1010 9         386 my $package_name = $self->{template_loader}->template_package_name($normalized_template_id);
1011            
1012             # FIGURE OUT WHERE WE'D CACHE THIS THING
1013 9         58 my $template_compiled_fn = $self->get_template_compiled_fn($package_name);
1014            
1015             # LOOK IN OUR CACHE TO SEE IF WE HAVE THE TEMPLATE
1016 9         23 my $cache_time_stamp = 0;
1017 9 100       437 if (-r $template_compiled_fn) {
1018 1         20 $cache_time_stamp = (stat($template_compiled_fn))[9];
1019             }
1020            
1021             # ATTEMPT TO LOAD THE TEMPLATE
1022 9         79 my ($template_lines, $tmojo_lite) = $self->{template_loader}->load_template($normalized_template_id, $cache_time_stamp);
1023            
1024             # IF $template_lines CAME BACK AS A ZERO, THEN OUR CACHED VERSION IS STILL GOOD
1025 9         28 my $cache_level = 0;
1026 9 100       53 if ($template_lines == 0) {
1027 1         2 $cache_level = 1;
1028            
1029 1 50       5 if (exists $memory_cache{$package_name}) {
1030 1 50       5 if ($cache_time_stamp <= $memory_cache{$package_name}) {
1031 1         2 $cache_level = 2;
1032             }
1033             }
1034             }
1035            
1036             # IF WE DON'T HAVE IT IN THE CACHE
1037 9 100       38 if ($cache_level == 0) {
1038            
1039             # COMPILE THE TEMPLATE
1040 8         12 my $template_compiled;
1041 8 50       27 if ($tmojo_lite) {
1042 0         0 $template_compiled = compile_lite_template($template_lines, $normalized_template_id, $package_name);
1043             }
1044             else {
1045 8         75 $template_compiled = compile_template($template_lines, $normalized_template_id, $package_name);
1046             }
1047            
1048             # CACHE THE TEMPLATE
1049             # ------------------
1050             # IT TURNS OUT THAT YOU CAN'T GET AWAY WITH HAVING THE LINE
1051             # NUMBERS IN THE PERL CODE, BECAUSE IT SCREWS UP qq{} AND
1052             # OTHER NEATO THINGS
1053            
1054             # SO, ALAS, NOW THAT WE'VE GONE TO THE TROUBLE OF ADDING THE
1055             # LINE NUMBERS, WE'RE GOING TO STRIP THEM AND PUT THEM IN
1056             # ANOTHER FILE... :(
1057 8         272 my @final_lines = split /\n/, $template_compiled;
1058            
1059 8 50       1142 open CODE_FH, ">$template_compiled_fn" or die "$! ($template_compiled_fn)";
1060 8 50       729 open LINE_FH, ">$template_compiled_fn.lines" or die "$! ($template_compiled_fn.lines)";
1061            
1062 8         36 foreach my $line (@final_lines) {
1063 550 100       1363 if ($line =~ /^(.*)(###TMOJO_(TEMPLATE_ID|LINE): .+)$/) {
1064 36         247 print CODE_FH "$1\n";
1065 36         224 print LINE_FH "$2\n";
1066             }
1067             else {
1068 514         841 print CODE_FH "$line\n";
1069 514         843 print LINE_FH ".\n";
1070             }
1071             }
1072            
1073 8         811 close CODE_FH;
1074 8         368 close LINE_FH;
1075             }
1076            
1077             # IF IT'S NOT IN THE MEMORY CACHE
1078 9 100       56 if ($cache_level < 2) {
1079             # DELETE THE PACKAGE
1080 8         71 delete_package($package_name);
1081            
1082             # PUT A CONTEXTUAL TMOJO OBJECT INTO THE PACKAGE
1083             {
1084 5     5   41 no strict 'refs';
  5         12  
  5         648  
  8         378  
1085 8         23 my $context_path = $normalized_template_id;
1086 8         165 $context_path =~ s{/[^/]+$}{};
1087 8         100 my $contextual_tmojo = HTML::Tmojo->new(%$self, context_path => $context_path);
1088 8         18 ${$package_name . '::Tmojo'} = $contextual_tmojo;
  8         118  
1089             }
1090            
1091             # NOW WE DO THE FILE
1092 8         17738 do $template_compiled_fn;
1093 8 50       5155 die if $@;
1094            
1095             # REMOVE THE TEMPLATE FROM %INC (BECAUSE StatINC HURTS)
1096 8         43 delete $INC{$template_compiled_fn};
1097            
1098             # RECORD THE LAST TIME THAT THE PACKAGE WAS COMPILED
1099 8         75 $memory_cache{$package_name} = time();
1100             }
1101            
1102             # MAKE SURE THAT LOAD TEMPLATE HAS BEEN CALLED ON THE PARENT TEMPLATES
1103             {
1104 5     5   22 no strict 'refs';
  5         17  
  5         2761  
  9         20  
1105            
1106             # MAKE SURE THAT WE DON'T HAVE AN INFINITE LOOP HERE
1107 9 50       37 if (defined $used_parents) {
1108 0 0       0 if ($used_parents->{$normalized_template_id} == 1) {
1109 0         0 die "circular parent reference, $normalized_template_id already used (this will cause an infinite loop)";
1110             }
1111             }
1112             else {
1113 9         28 $used_parents = {};
1114             }
1115            
1116 9         33 $used_parents->{$normalized_template_id} = 1;
1117            
1118 9         16 my @parents = @{$package_name . '::TMOJO_ISA'};
  9         90  
1119            
1120 9 50       57 if (@parents) {
1121 0         0 foreach (@parents) {
1122 0         0 my $contextual_tmojo = ${$package_name . '::Tmojo'};
  0         0  
1123 0         0 $_ = $contextual_tmojo->get_template_class($_, $used_parents);
1124             }
1125            
1126 0         0 @{$package_name . '::ISA'} = @parents;
  0         0  
1127             }
1128             }
1129            
1130             # RETURN THE PACKAGE NAME
1131 9         69 return $package_name;
1132             }
1133              
1134             sub normalize_template_id {
1135 18     18 0 56 my ($self, $template_id) = @_;
1136            
1137             # THIS IS WHERE THE MAGIC OF THE CONTEXT PATH IS RESOLVED
1138 18 50       213 if (substr($template_id, 0, 3) eq '../') {
    100          
1139 0         0 my $context_path = $self->{context_path};
1140            
1141 0         0 while (substr($template_id, 0, 3) eq '../') {
1142 0         0 $context_path =~ s{/[^/]*$}{};
1143 0         0 $template_id = substr($template_id, 3);
1144             }
1145            
1146 0         0 $template_id = "$context_path/$template_id";
1147             }
1148             elsif (substr($template_id, 0, 1) ne '/') {
1149 16         244 $template_id = "$self->{context_path}/$template_id";
1150             }
1151            
1152             # HANDLE UPWARD TRAVERSAL
1153 18 50       96 if (substr($template_id, -1, 1) eq '^') {
1154 0         0 $template_id = substr($template_id, 0, -1);
1155            
1156 0         0 while (rindex($template_id, '/') > 0) {
1157 0 0       0 if ($self->{template_loader}->template_exists($template_id)) {
1158 0         0 last;
1159             }
1160             else {
1161 0         0 $template_id =~ s{/[^/]+/([^/]+)$}{/$1};
1162             }
1163             }
1164             }
1165            
1166             # NOW WE'VE GOT OUR NAME
1167 18         92 return $template_id;
1168             }
1169              
1170             sub get_template_compiled_fn {
1171 9     9 0 26 my ($self, $package_name) = @_;
1172            
1173             # MAKE SURE ALL OF THE DIRECTORIES ARE THERE
1174 9         341 my @parts = split('::', $package_name);
1175 9         40 my $current_dir = $self->{cache_dir};
1176            
1177             # GET RID OF THE LAST ONE
1178 9         34 my $last_part = pop @parts;
1179            
1180             # MAKE ALL OF THE DIRECTORIES
1181 9         68 while (@parts) {
1182 45         335 $current_dir .= '/' . shift(@parts);
1183 45 100       1455 unless (-d $current_dir) {
1184 25         2241 mkdir $current_dir;
1185             }
1186             }
1187            
1188 9         364 my $compiled_fn = $current_dir . "/$last_part";
1189            
1190 9         42 return $compiled_fn;
1191             }
1192              
1193             1;