1package HTML::Template::JIT::Compiler;
2
3use 5.006;
4use strict;
5use warnings;
6
7our $VERSION = '0.01';
8
9use HTML::Template;
10use Carp qw(croak confess);
11use File::Path qw(mkpath rmtree);
12
13sub compile {
14  my %args = @_;
15  my $self = bless({});
16
17  # parse the template as usual
18  $self->{template} = HTML::Template->new(%args);
19
20  # setup state
21  $self->{jit_path}          = $args{jit_path};
22  $self->{package}           = $args{package};
23  $self->{package_dir}       = $args{package_dir};
24  $self->{package_path}      = $args{package_path};
25  $self->{jit_pool}          = [];
26  $self->{jit_sym}           = 0;
27  $self->{jit_debug}         = $args{jit_debug};
28  $self->{text_size}         = 0;
29  $self->{loop_context_vars} = $args{loop_context_vars};
30  $self->{max_depth}         = 0;
31  $self->{global_vars}       = $args{global_vars};
32  $self->{print_to_stdout}   = $args{print_to_stdout};
33  $self->{case_sensitive}    = $args{case_sensitive};
34
35  # compile internal representation into a chunk of C code
36
37  # get code for param function
38  my @code = $self->_output();
39
40  if ($self->{jit_debug}) {
41    print STDERR "###################### CODE START ######################\n\n";
42    open(INDENT, "| indent > code.tmp");
43    print INDENT join("\n", @code);
44    close INDENT;
45    open(CODE, 'code.tmp');
46    print STDERR join('', <CODE>);
47    close(CODE);
48    unlink('code.tmp');
49    print STDERR "\n\n###################### CODE END ######################\n\n";
50  }
51
52  $self->_write_module(\@code);
53
54  # try to load the module and return package handle if successful
55  my $result;
56  eval { $result = require $self->{package_path}; };
57  return 1 if $result;
58
59  # don't leave failed compiles lying around unless we're debuging
60  rmtree($self->{package_dir}, 0, 0) unless $self->{jit_debug};
61  die $@ if $@;
62  return 0;
63}
64
65# writes out the module file
66sub _write_module {
67  my ($self, $code) = @_;
68
69  # make directory
70  mkpath($self->{package_dir}, 0, 0700);
71
72  # open module file
73  open(MODULE, ">$self->{package_path}") or die "Unable to open $self->{package_path} for output : $!";
74
75  my $inline_debug = "";
76  my $optimize = "-O3";
77  if ($self->{jit_debug}) {
78    $inline_debug = ", CLEAN_AFTER_BUILD => 0";
79    $optimize = "-g";
80  }
81
82  # print out preamble
83  print MODULE <<END;
84package $self->{package};
85use base 'HTML::Template::JIT::Base';
86
87use Inline C => Config => OPTIMIZE => "$optimize", DIRECTORY => "$self->{package_dir}" $inline_debug;
88use Inline C => <<'CODE_END';
89
90END
91
92  # print out code
93  print MODULE join("\n", @$code), "\nCODE_END\n";
94
95  # output the param hash
96  print MODULE "our \%param_hash = (\n", join(',', $self->_param_hash([])), ");\n";
97
98  # empty param map
99  print MODULE "our \%param_map;\n";
100
101  # note case sensitivity
102  print MODULE "our \$case_sensitive = $self->{case_sensitive};\n";
103
104  print MODULE "\n1;\n";
105
106  # all done
107  close MODULE;
108}
109
110# construct the output function
111sub _output {
112  my $self = shift;
113  my $template = $self->{template};
114
115  # construct body of output
116  my @code = $self->_output_template($template, 0);
117
118  # write global pool
119  unshift @code, '', $self->_write_pool();
120
121  # setup result size based on gathered stats with a little extra for variables
122  my $size = int ($self->{text_size} + ($self->{text_size} * .10));
123
124  # head code for output function, deferred to allow for $size and
125  # max_depth setup
126  unshift @code, <<END;
127SV * output(SV *self) {
128  SV *result = NEWSV(0, $size);
129  HV *param_map[($self->{max_depth} + 1)];
130  SV ** temp_svp;
131  SV * temp_sv;
132  int i;
133  STRLEN len;
134  unsigned char c;
135  char buf[4];
136
137  SvPOK_on(result);
138  param_map[0] = get_hv("$self->{package}::param_map", 0);
139
140END
141
142  # finish output function
143  push @code, "return result;", "}";
144
145  return @code;
146}
147
148# output the body of a single scope (top-level or loop)
149sub _output_template {
150  my ($self, $template, $offset) = @_;
151  $self->{max_depth} = $offset
152    if $offset > $self->{max_depth};
153
154  my (@code, @top, %vars, @pool, %blocks, $type, $name, $var,
155      $do_escape, $has_default);
156
157  # setup some convenience aliases ala HTML::Template::output()
158  use vars qw($line  @parse_stack  %param_map);
159  local      (*line, *parse_stack, *param_map);
160  *parse_stack = $template->{parse_stack};
161  *param_map   = $template->{param_map};
162
163  my %reverse_param_map = map { $param_map{$_} => $_ } keys %param_map;
164  my $parse_stack_length = $#parse_stack;
165
166  for (my $x = 0; $x <= $parse_stack_length; $x++) {
167    *line = \$parse_stack[$x];
168    $type = ref($line);
169
170    # need any block closings on this line?
171    push(@code, "}" x $blocks{$x}) if $blocks{$x};
172
173    if ($type eq 'SCALAR') {
174      # append string and add size to text_size counter
175      if ($self->{print_to_stdout}) {
176        push @code, _print_string($$line);
177      } else {
178        push @code, _concat_string($$line);
179        $self->{text_size} += length $$line;
180      }
181
182    } elsif ($type eq 'HTML::Template::VAR') {
183      # get name for this variable from reverse map
184      $name = $reverse_param_map{$line};
185
186      # check var cache - can't use it for escaped variables
187      if (exists $vars{$name}) {
188        $var = $vars{$name};
189      }
190
191      # load a new one if needed
192      else {
193        $var = $self->_get_var("SV *", "&PL_sv_undef", \@pool);
194        push @top, _load_var($name, $var, $offset, $self->{global_vars});
195        $vars{$name} = $var;
196      }
197
198      # escape var if needed
199      if ($do_escape) {
200        push @code, _escape_var($var, $do_escape);
201      }
202
203      # append the var
204      push @code, ($self->{print_to_stdout} ? _print_var($var,  $do_escape, $has_default) :
205                                              _concat_var($var, $do_escape, $has_default));
206
207      # reset flags
208      undef $do_escape;
209      undef $has_default;
210
211    } elsif ($type eq 'HTML::Template::DEFAULT') {
212      $has_default = $$line;
213
214    } elsif ($type eq 'HTML::Template::LOOP') {
215      # get loop template
216      my $loop_template = $line->[HTML::Template::LOOP::TEMPLATE_HASH]{$x};
217
218      # allocate an hv for the loop param_map
219      my $loop_offset = $offset + 1;
220
221      # remember text_size before loop
222      my $old_text_size = $self->{text_size};
223
224      # output the loop start
225      push @code, $self->_start_loop($reverse_param_map{$line}, $offset,
226				     $loop_offset);
227
228      # output the loop code
229      push @code, $self->_output_template($loop_template, $loop_offset);
230
231      # send the loop
232      push @code, $self->_end_loop();
233
234      # guesstimate average loop run of 10 and pre-allocate space for
235      # text accordingly.  This is a bit silly but something has to be
236      # done to account for loops increasing result size...
237      $self->{text_size} += (($self->{text_size} - $old_text_size) * 9);
238
239    } elsif ($type eq 'HTML::Template::COND') {
240      # if, unless and else
241
242      # store block end loc
243      $blocks{$line->[HTML::Template::COND::JUMP_ADDRESS]}++;
244
245      # get name for this var
246      $name = $reverse_param_map{$line->[HTML::Template::COND::VARIABLE]};
247
248      # load a new var unless we have this one
249      if (exists $vars{$name}) {
250        $var = $vars{$name};
251      } else {
252        $var = $self->_get_var("SV *", "&PL_sv_undef", \@pool);
253        push @top, _load_var($name, $var, $offset, $self->{global_vars});
254        $vars{$name} = $var;
255      }
256
257      # output conditional
258      push(@code, $self->_cond($line->[HTML::Template::COND::JUMP_IF_TRUE],
259			       $line->[HTML::Template::COND::VARIABLE_TYPE] == HTML::Template::COND::VARIABLE_TYPE_VAR,
260                               $var,
261                               $line->[HTML::Template::COND::UNCONDITIONAL_JUMP],
262			      ));
263    } elsif ($type eq 'HTML::Template::ESCAPE') {
264      $do_escape = 'HTML';
265    } elsif ($type eq 'HTML::Template::URLESCAPE') {
266      $do_escape = 'URL';
267    } elsif ($type eq 'HTML::Template::JSESCAPE') {
268      $do_escape = 'JS';
269    } elsif ($type eq 'HTML::Template::NOOP') {
270      # noop
271    } else {
272      confess("Unsupported object type in parse stack : $type");
273    }
274  }
275
276  # output pool of variables used in body
277  unshift @code, '{', $self->_write_pool(\@pool), @top;
278  push @code, '}';
279
280  return @code;
281}
282
283# output a conditional expression
284sub _cond {
285  my ($self, $is_unless, $is_var, $var, $is_uncond) = @_;
286  my @code;
287
288  if ($is_uncond) {
289    push(@code, "else {");
290  } else {
291    if ($is_var) {
292      if ($is_unless) {
293        # unless var
294        push(@code, "if (!SvTRUE($var)) {");
295      } else {
296        # if var
297        push(@code, "if (SvTRUE($var)) {");
298      }
299    } else {
300      if ($is_unless) {
301        # unless loop
302        push(@code, "if ($var == &PL_sv_undef || av_len((AV *) SvRV($var)) == -1) {");
303      } else {
304        # if loop
305        push(@code, "if ($var != &PL_sv_undef && av_len((AV *) SvRV($var)) != -1) {");
306      }
307    }
308  }
309
310  return @code;
311}
312
313# start a loop
314sub _start_loop {
315  my ($self, $name, $offset, $loop_offset) = @_;
316  my $name_string = _quote_string($name);
317  my $name_len    = length($name_string);
318  my @pool;
319  my $av          = $self->_get_var("AV *", 0, \@pool);
320  my $av_len      = $self->_get_var("I32", 0, \@pool);
321  my $counter     = $self->_get_var("I32", 0, \@pool);
322  my @code;
323
324  my $odd;
325  if ($self->{loop_context_vars}) {
326    $odd = $self->_get_var("I32", 0, \@pool);
327    push(@code, "$odd = 0;");
328  }
329
330  push @code, <<END;
331temp_svp = hv_fetch(param_map[$offset], "$name_string", $name_len, 0);
332if (temp_svp && (*temp_svp != &PL_sv_undef)) {
333   $av = (AV *) SvRV(*temp_svp);
334   $av_len = av_len($av);
335
336   for($counter = 0; $counter <= $av_len; $counter++) {
337      param_map[$loop_offset] = (HV *) SvRV(*(av_fetch($av, $counter, 0)));
338END
339
340  if ($self->{loop_context_vars}) {
341    push @code, <<END;
342      if ($counter == 0) {
343	hv_store(param_map[$loop_offset], "__first__", 9, &PL_sv_yes, 0);
344	hv_store(param_map[$loop_offset], "__inner__", 9, &PL_sv_no, 0);
345	if ($av_len == 0)
346        hv_store(param_map[$loop_offset], "__last__",  8,  &PL_sv_yes, 0);
347      } else if ($counter == $av_len) {
348        hv_store(param_map[$loop_offset], "__first__", 9, &PL_sv_no, 0);
349        hv_store(param_map[$loop_offset], "__inner__", 9, &PL_sv_no, 0);
350        hv_store(param_map[$loop_offset], "__last__",  8,  &PL_sv_yes, 0);
351      } else {
352        hv_store(param_map[$loop_offset], "__first__", 9, &PL_sv_no, 0);
353        hv_store(param_map[$loop_offset], "__inner__", 9, &PL_sv_yes, 0);
354        hv_store(param_map[$loop_offset], "__last__",  8,  &PL_sv_no, 0);
355      }
356
357      hv_store(param_map[$loop_offset], "__odd__", 7, (($odd = !$odd) ? &PL_sv_yes : &PL_sv_no), 0);
358      hv_store(param_map[$loop_offset], "__counter__", 11, newSViv($counter + 1), 0);
359END
360
361  }
362
363  unshift @code, "{", $self->_write_pool(\@pool);
364
365  return @code;
366}
367
368# end a loop
369sub _end_loop {
370  return '}}}';
371}
372
373# construct %param_hash
374sub _param_hash {
375  my ($self, $path) = @_;
376  my $template = $self->{template};
377
378  my @params;
379  if (@$path) {
380    @params = $template->query(LOOP => $path);
381  } else {
382    @params = $template->param();
383  }
384
385  my @out;
386  foreach my $name (@params) {
387    my $type = $template->query(name => [ @$path, $name ]);
388    if ($type eq 'VAR') {
389      push @out, "'$name'", 1;
390    } else {
391      push @out, "'$name'", "\n{" . join(', ', $self->_param_hash([ @$path, $name ])) . "\n}\n";
392    }
393  }
394
395  return @out;
396}
397
398
399# get a fresh var of the requested C type from the pool
400sub _get_var {
401  my ($self, $type, $default, $pool) = @_;
402  $pool = $self->{jit_pool} unless defined $pool;
403  my $sym = "sym_" . $self->{jit_sym}++;
404  push @$pool, $type, ($default ? "$sym = $default" : $sym);
405  return $sym;
406}
407
408# write out the code to initialize the pool
409sub _write_pool {
410  my ($self, $pool) = @_;
411  $pool = $self->{jit_pool} unless defined $pool;
412  my @code;
413
414  for (my $index = 0; $index < @$pool; $index += 2) {
415      push(@code, $pool->[$index] . ' ' . $pool->[$index + 1] . ";");
416  }
417  @$pool = ();
418  return @code;
419}
420
421# concatenate a string onto result
422sub _concat_string {
423  return "" unless $_[0];
424  my $len = length($_[0]);
425  my $string = _quote_string($_[0]);
426
427  return "sv_catpvn(result, \"$string\", $len);"
428}
429
430# concatenate a string onto result
431sub _print_string {
432  return "" unless $_[0];
433  my $string = _quote_string($_[0]);
434  return "PerlIO_stdoutf(\"$string\");";
435}
436
437# loads a variable into a lexical pool variable
438sub _load_var {
439  my ($name, $var, $offset, $global) = @_;
440  my $string = _quote_string($name);
441  my $len    = length($name);
442
443  return <<END if $global and $offset;
444for (i = $offset; i >= 0; i--) {
445   if (hv_exists(param_map[i], "$string", $len)) {
446      $var = *(hv_fetch(param_map[i], "$string", $len, 0));
447      if ($var != &PL_sv_undef) break;
448   }
449}
450END
451
452  return <<END;
453if (hv_exists(param_map[$offset], "$string", $len))
454   $var = *(hv_fetch(param_map[$offset], "$string", $len, 0));
455END
456}
457
458# loads a variable and escapes it
459sub _escape_var {
460  my ($var, $escape) = @_;
461
462  # apply escaping to a mortal copy in temp_sv
463  my @code = (<<END);
464if ($var != &PL_sv_undef) {
465  SvPV_force($var, len);
466  temp_sv = sv_mortalcopy($var);
467  len = 0;
468  while (len < SvCUR(temp_sv)) {
469    c = *(SvPVX(temp_sv) + len);
470END
471
472  # perform the appropriate escapes
473  if ($escape eq 'HTML') {
474      push @code, <<END;
475    switch (c) {
476      case '&':
477        sv_insert(temp_sv, len, 1, "&amp;",  5);
478        len += 4;
479        break;
480      case '"':
481        sv_insert(temp_sv, len, 1, "&quot;", 6);
482        len += 5;
483        break;
484      case '>':
485        sv_insert(temp_sv, len, 1, "&gt;",   4);
486        len += 3;
487        break;
488      case '<':
489        sv_insert(temp_sv, len, 1, "&lt;",   4);
490        len += 3;
491        break;
492      case '\\'':
493        sv_insert(temp_sv, len, 1, "&#39;",  5);
494        len += 4;
495        break;
496    }
497END
498  } elsif ($escape eq 'URL') {
499      push @code, <<END;
500    if (!(isALNUM(c) || (c == '-') || (c == '.'))) {
501       sprintf(buf, "%%%02X", c);
502       sv_insert(temp_sv, len, 1, buf, 3);
503       len += 2;
504    }
505END
506  } elsif ($escape eq 'JS') {
507      push @code, <<'END';
508    switch (c) {
509      case '\\':
510      case '\'':
511      case '"':
512        sprintf(buf, "\\%c", c);
513        sv_insert(temp_sv, len, 1, buf, 2);
514        len += 1;
515        break;
516      case '\n':
517        sprintf(buf, "\\n");
518        sv_insert(temp_sv, len, 1, buf, 2);
519        len += 1;
520        break;
521      case '\r':
522        sprintf(buf, "\\r");
523        sv_insert(temp_sv, len, 1, buf, 2);
524        len += 1;
525    }
526END
527
528  } else {
529    die "Unknown escape type '$escape'.";
530  }
531
532  push @code, <<END;
533    len++;
534  }
535}
536END
537
538  return @code;
539}
540
541# concatenate a var onto result
542sub _concat_var {
543  return "if ($_[0] != &PL_sv_undef) sv_catsv(result, " .
544    ($_[1] ? "temp_sv" : $_[0]) . ");" .
545        (defined $_[2] ? " else " . _concat_string($_[2]) : "");
546}
547
548# print a var to stdout
549sub _print_var {
550  return "if ($_[0] != &PL_sv_undef) PerlIO_stdoutf(SvPV_nolen(" .
551    ($_[1] ? "temp_sv" : $_[0]) . "));" .
552      (defined $_[2] ? " else " . _print_string($_[2]) : "");
553}
554
555# turn a string into something that C will accept inside
556# double-quotes.  or should I go the array of bytes route?  I think
557# that might be the only way to get UTF-8 working.  It's such hell to
558# debug though...
559sub _quote_string {
560  my $string = shift;
561  $string    =~ s/\\/\\\\/g;
562  $string    =~ s/"/\\"/g;
563  $string    =~ s/\r/\\r/g;
564  $string    =~ s/\n/\\n/g;
565  $string    =~ s/\t/\\t/g;
566  return $string;
567}
568
5691;
570
571__END__
572
573=pod
574
575=head1 NAME
576
577HTML::Template::JIT::Compiler - Compiler for HTML::Template::JIT
578
579=head1 SYNOPSIS
580
581  use HTML::Template::JIT::Compiler;
582
583  HTML::Template::JIT->compile(...);
584
585=head1 DESCRIPTION
586
587This module is used internally by HTML::Template::JIT to compile
588template files.  Don't use it directly - use HTML::Template::JIT
589instead.
590
591=head1 AUTHOR
592
593Sam Tregar <sam@tregar.com>
594
595=head1 LICENSE
596
597HTML::Template::JIT : Just-in-time compiler for HTML::Template
598
599Copyright (C) 2001 Sam Tregar (sam@tregar.com)
600
601This module is free software; you can redistribute it and/or modify it
602under the terms of either:
603
604a) the GNU General Public License as published by the Free Software
605Foundation; either version 1, or (at your option) any later version,
606or
607
608b) the "Artistic License" which comes with this module.
609
610This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied warranty of
611MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See either
612the GNU General Public License or the Artistic License for more details.
613
614You should have received a copy of the Artistic License with this
615module, in the file ARTISTIC.  If not, I'll be glad to provide one.
616
617You should have received a copy of the GNU General Public License
618along with this program; if not, write to the Free Software
619Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
620USA
621
622