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, "&", 5); 478 len += 4; 479 break; 480 case '"': 481 sv_insert(temp_sv, len, 1, """, 6); 482 len += 5; 483 break; 484 case '>': 485 sv_insert(temp_sv, len, 1, ">", 4); 486 len += 3; 487 break; 488 case '<': 489 sv_insert(temp_sv, len, 1, "<", 4); 490 len += 3; 491 break; 492 case '\\'': 493 sv_insert(temp_sv, len, 1, "'", 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