## ## copyright(c) 2007-2008 kuwata-lab.com all rights reserved. ## ## Permission is hereby granted, free of charge, to any person obtaining ## a copy of this software and associated documentation files (the ## "Software"), to deal in the Software without restriction, including ## without limitation the rights to use, copy, modify, merge, publish, ## distribute, sublicense, and/or sell copies of the Software, and to ## permit persons to whom the Software is furnished to do so, subject to ## the following conditions: ## ## The above copyright notice and this permission notice shall be ## included in all copies or substantial portions of the Software. ## ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND ## NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE ## LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION ## OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION ## WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## ## $Rev: 37 $ ## $Release: 0.0.2 $ package Tenjin; #use strict; $Tenjin::USE_STRICT = 0; $Tenjin::TEMPLATE_CLASS = 'Tenjin::Template'; $Tenjin::CONTEXT_CLASS = 'Tenjin::Context'; ## ## utility package ## package Tenjin::Util; sub read_file { my ($filename, $lock_required) = @_; open(IN, $filename) or die("$filename: $!"); binmode(IN); my $content = ''; my $size = 8192; my @buf = (); flock(IN, 1) if ($lock_required); while (read(IN, my $data, $size)) { push(@buf, $data); } close(IN); return $#buf == 0 ? $buf[0] : join('', @buf); } sub write_file { my ($filename, $content, $lock_required) = @_; open(OUT, ">$filename") or die("$filename: $!"); binmode(OUT); flock(OUT, 2) if $lock_required; print OUT $content; close(OUT); } sub expand_tabs { my ($str, $tabwidth) = @_; $tabwidth = 8 unless defined($tabwidth); my @buf = (); my $pos = 0; while ($str =~ /.*?\t/sg) { ## /(.*?)\t/ may be slow my $end = $+[0]; my $text = substr($str, $pos, $end - 1 - $pos); my $n = rindex($text, "\n"); my $col = $n >= 0 ? length($text) - $n - 1 : length($text); push(@buf, $text, ' ' x ($tabwidth - $col % $tabwidth)); $pos = $end; } my $rest = substr($str, $pos); push(@buf, $rest) if $rest; return join('', @buf); } sub _p { my ($arg) = @_; return "<`\#$arg\#`>" } sub _P { my ($arg) = @_; return "<`\$$arg\$`>" } sub _decode_params { my ($s) = @_; $s = '' . $s; return '' unless $s; $_ = $s; s/%3C%60%23(.*?)%23%60%3E/'[=='.Tenjin::Helper::Html::decode_url($1).'=]'/ge; s/%3C%60%24(.*?)%24%60%3E/'[='.Tenjin::Helper::Html::decode_url($1).'=]'/ge; s/<`\#(.*?)\#`>/'[=='.Tenjin::Helper::Html::unescape_xml($1).'=]'/ge; s/<`\$(.*?)\$`>/'[='.Tenjin::Helper::Html::unescape_xml($1).'=]'/ge; s/<`\#(.*?)\#`>/[==$1=]/g; s/<`\$(.*?)\$`>/[=$1=]/g; return $_; } ## ## HTML Helper ## package Tenjin::Helper::Html; %Tenjin::Helper::Html::_escape_table = ( '&'=>'&', '<'=>'<', '>'=>'>', '"'=>'"', "'"=>'''); sub escape_xml { my ($s) = @_; #return HTML::Entities::encode_entities($s); $s =~ s/[&<>"]/$Tenjin::Helper::Html::_escape_table{$&}/ge if ($s); return $s; } %Tenjin::Helper::Html::_unescape_table = ('lt'=>'<', 'gt'=>'>', 'amp'=>'&', 'quot'=>'"', '#039'=>"'"); sub unescape_xml { my ($s) = @_; $s =~ tr/+/ /; $s =~ s/&(lt|gt|amp|quot|#039);/$Tenjin::Helper::Html::_unescape_table{$1}/ge if ($s); return $s; } sub encode_url { my ($s) = @_; $s =~ s/([^-A-Za-z0-9_.\/])/sprintf("%%%02X", ord($1))/sge; $s =~ tr/ /+/; return $s; } sub decode_url { my ($s) = @_; #$s =~ s/\%([a-fA-F0-9][a-fA-F0-9])/pack('H2', $1)/sge; $s =~ s/\%([a-fA-F0-9][a-fA-F0-9])/pack('C', hex($1))/sge; return $s; } sub checked { my ($expr) = @_; return $expr ? ' checked="checked"' : ''; } sub selected { my ($expr) = @_; return $expr ? ' selected="selected"' : ''; } sub disabled { my ($expr) = @_; return $expr ? ' disabled="disabled"' : ''; } sub nl2br { my ($text) = @_; #$text = Tenjin::Helper::Html::escape_xml($text); $text =~ s/(\r?\n)/
$1/g; return $text; } sub text2html { my ($text) = @_; $text = Tenjin::Helper::Html::escape_xml($text); $text =~ s/(\r?\n)/
$1/g; return $text; } sub tagattr { ## [experimental] my ($name, $expr, $value) = @_; return '' unless $expr; $value = $expr unless defined($value); return " $name=\"$value\""; } ## ## base colass of context object ## package Tenjin::BaseContext; sub new { my $class = shift; my ($this) = @_; $this = { } unless defined($this); return bless($this, $class); } ## ex. {'x'=>10, 'y'=>20} ==> "my $x = $_context->{'x'}; my $y = $_context->{'y'}; " sub _build_decl { my ($context) = @_; my @buf = (); while (my ($k, ) = each %$context) { push(@buf, "my \$$k = \$_context->{'$k'}; ") if $k ne '_context'; } return join('', @buf); } $Tenjin::BaseContext::defun = <<'END'; sub evaluate { my $this = shift; my ($_script) = @_; my $_context = $this; #$_script = Tenjin::BaseContext::_build_decl($_context) . $_script if ($_context); return eval $_script unless $Tenjin::USE_STRICT; use strict; return eval $_script; } sub to_func { #my ($_script, $_filename) = @_; #$script =~ s/join\('', \@_buf\);\s*\Z/return $&/; #if ($Tenjin::USE_STRICT) { ## NOT WORK # use strict; #} #my $_func = eval("sub { my (\$_context) = \@_; $_script }"); my $_klass = shift; my ($_script) = @_; my $_s = "sub { my (\$_context) = \@_; $_script }"; my $_func; if ($Tenjin::USE_STRICT) { use strict; $_func = eval($_s); } else { $_func = eval($_s); } #$@ and die("*** Compile Error: " . $_filename . "\n", $@); return $_func; } END eval $Tenjin::BaseContext::defun; sub escape { my ($arg) = @_; return $arg; } *_p = *Tenjin::Util::_p; *_P = *Tenjin::Util::_P; ## ## common context object class which supports HTML helpers ## package Tenjin::Context; #@ISA = ('Tenjin::BaseContext'); @Tenjin::Context::ISA = ('Tenjin::BaseContext'); $Tenjin::Context::defun = $Tenjin::BaseContext::defun; eval $Tenjin::Context::defun; *_p = *Tenjin::Util::_p; *_P = *Tenjin::Util::_P; *escape = *Tenjin::Helper::Html::escape_xml; *escape_xml = *Tenjin::Helper::Html::escape_xml; *encode_url = *Tenjin::Helper::Html::encode_url; *checked = *Tenjin::Helper::Html::checked; *selected = *Tenjin::Helper::Html::selected; *disabled = *Tenjin::Helper::Html::disabled; *nl2br = *Tenjin::Helper::Html::nl2br; *text2html = *Tenjin::Helper::Html::text2html; *tagattr = *Tenjin::Helper::Html::tagattr; ## ## template class ## ## ex. ## ## convert file into perl script ## use Tenjin; ## $Tenjin::USE_STRICT = 1; ## optional ## my $template = new Tenjin::Template('example.plhtml'); ## print $template->{script}; ## ## or ## my $template = new Tenjin::Template(); ## my $input = Tenjin::Util::read_file('example.plhtml'); ## print $template->convert($input, 'example.plhtml'); # filename is optional ## ## evaluate converted perl script with context data ## my $context = { 'title'=>'Example', 'items'=>['A','B','C'], }; ## #$template->compile(); ## optional ## print $template->render($context); ## package Tenjin::Template; sub new { my $class = shift; my ($filename, $opts) = @_; my $escapefunc = defined($opts) && exists($opts->{escapefunc}) ? $opts->{escapefunc} : 'escape'; my $this = { 'filename' => $filename, 'script' => undef, 'escapefunc' => $escapefunc, 'timestamp' => undef, 'args' => undef, }; #return bless($this, $class); $this = bless($this, $class); if ($filename) { $this->convert_file($filename); }; return $this; } sub _render { my $this = shift; my ($context) = (@_); $context = {} unless $context; if ($this->{func}) { return $this->{func}->($context); } else { if (ref($context) eq 'HASH') { my $klass = $Tenjin::CONTEXT_CLASS; # || Tenjin::Context; $context = $klass->new($context); } my $script = $this->{script}; $script = Tenjin::BaseContext::_build_decl($context) . $script unless ($this->{args}); return $context->evaluate($script); } } sub render { my $this = shift; #my $output = $this->{func} ? $this->{func}->(@_) : $this->_render(@_); my $output = $this->_render(@_); if ($@) { # error happened my $template_filename = $this->{filename}; die "*** ERROR: " . $this->{filename} . "\n", $@; } return $output; } sub convert_file { my $this = shift; my ($filename) = @_; my $input = Tenjin::Util::read_file($filename, 1); my $script = $this->convert($input); $this->{filename} = $filename; #$this->{input} = $input; return $script; } sub convert { my $this = shift; my ($input, $filename) = @_; $this->{filename} = $filename; my @buf = ('my @_buf = (); ', ); $this->parse_stmt(\@buf, $input); push(@buf, "join('', \@_buf);\n"); #push(@buf, "\n\\\@_buf;\n"); return $this->{script} = join('', @buf); } sub compile_stmt_pattern { my ($pi) = @_; my $pat = '((^[ \t]*)?<\?'.$pi.'( |\t|\r?\n)(.*?) ?\?>([ \t]*\r?\n)?)'; return qr/$pat/sm; } #my $STMT_PATTERN = qr/((^[ \t]*)?<\?pl( |\t|\r?\n)(.*?) ?\?>([ \t]*\r?\n)?)/sm; my $STMT_PATTERN = compile_stmt_pattern('pl'); sub stmt_pattern { my $this = shift; return $STMT_PATTERN; } sub parse_stmt { my $this = shift; my ($bufref, $input) = @_; my $pos = 0; my $pat = $this->stmt_pattern(); while ($input =~ /$pat/g) { my ($pi, $lspace, $mspace, $stmt, $rspace) = ($1, $2, $3, $4, $5); my $start = $-[0]; my $text = substr($input, $pos, $start - $pos); $pos = $start + length($pi); if ($text) { $this->parse_expr($bufref, $text); } $mspace = '' if $mspace eq ' '; $stmt = $this->hook_stmt($stmt); $this->add_stmt($bufref, $lspace . $mspace . $stmt . $rspace); } my $rest = $pos == 0 ? $input : substr($input, $pos); $this->parse_expr($bufref, $rest) if $rest; } sub hook_stmt { my $this = shift; my ($stmt) = @_; ## macro expantion if ($stmt =~ /\A(\s*)(\w+)\((.*?)\);?(\s*)\Z/) { my ($lspace, $funcname, $arg, $rspace) = ($1, $2, $3, $4); my $s = $this->expand_macro($funcname, $arg); if (defined($s)) { return $lspace . $s . $rspace; } } ## template arguments if (! $this->{args}) { if ($stmt =~ m/\A(\s*)\#\@ARGS\s+(.*)(\s*)\Z/) { my ($lspace, $argstr, $rspace) = ($1, $2, $3); my @args = (); my @declares = (); for my $arg (split(',', $argstr)) { $arg =~ s/(^\s+|\s+$)//g; next unless $arg; $arg =~ m/\A[a-zA-Z_]\w*\Z/ or die("'$arg': invalid template argument."); push(@args, $arg); push(@declares, "my \$$arg = \$_context->{$arg}; "); } $this->{args} = \@args; return $lspace . join('', @declares) . $rspace; } } ## return $stmt; } $Tenjin::Template::MACRO_HANDLER_TABLE = { 'include' => sub { my ($arg) = @_; "push(\@_buf, \$_context->{_engine}->render($arg, \$_context, 0));"; }, 'start_capture' => sub { my ($arg) = @_; "my \@_buf_bkup=\@_buf; \@_buf=(); my \$_capture_varname=$arg;"; }, 'stop_capture' => sub { my ($arg) = @_; "\$_context->{\$_capture_varname}=join('',\@_buf); \@_buf=\@_buf_bkup;"; }, 'start_placeholder' => sub { my ($arg) = @_; "if (\$_context->{$arg}) { push(\@_buf,\$_context->{$arg}); } else {"; }, 'stop_placeholder' => sub { my ($arg) = @_; "}"; }, 'echo' => sub { my ($arg) = @_; "push(\@_buf, $arg);"; }, }; sub expand_macro { my $this = shift; my ($funcname, $arg) = @_; my $handler = $Tenjin::Template::MACRO_HANDLER_TABLE->{$funcname}; return $handler ? $handler->($arg) : undef; } my $EXPR_PATTERN = qr/\[=(=?)(.*?)(=?)=\]/s; sub expr_pattern { my $this = shift; return $EXPR_PATTERN; } ## ex. get_expr_and_escapeflag('=', '$item->{name}', '') => 1, '$item->{name}', 0 sub get_expr_and_escapeflag { my $this = shift; my ($m1, $m2, $m3) = @_; my ($not_escape, $expr, $delete_newline) = ($m1, $m2, $m3); return $expr, $not_escape eq '', $delete_newline eq '=', } sub parse_expr { my $this = shift; my ($bufref, $input) = @_; my $pos = 0; $this->start_text_part($bufref); my $pat = $this->expr_pattern(); while ($input =~ /$pat/g) { my $start = $-[0]; my $text = substr($input, $pos, $start - $pos); my ($expr, $flag_escape, $delete_newline) = $this->get_expr_and_escapeflag($1, $2, $3); $pos = $start + length($&); $this->add_text($bufref, $text) if ($text); $this->add_expr($bufref, $expr, $flag_escape) if $expr; if ($delete_newline) { my $end = $+[0]; if (substr($input, $end+1, 1) == "\n") { push(@$bufref, "\n"); $pos += 1; } } } my $rest = $pos == 0 ? $input : substr($input, $pos); $this->add_text($bufref, $rest); $this->stop_text_part($bufref); } sub start_text_part { my ($this) = shift; my ($bufref) = @_; push(@$bufref, "push(\@_buf, "); } sub stop_text_part { my ($this) = shift; my ($bufref) = @_; push(@$bufref, "); "); } sub add_text { my $this = shift; my ($bufref, $text) = @_; return unless $text; $text =~ s/[`\\]/\\$&/g; #push(@$bufref, "push(\@_buf, q`", $text, "`); "); push(@$bufref, "q`$text`, "); #push(@$bufref, "q`", $text, "`, "); } sub add_stmt { my $this = shift; my ($bufref, $stmt) = @_; push(@$bufref, $stmt); } sub add_expr { my $this = shift; my ($bufref, $expr, $flag_escape) = @_; if ($flag_escape) { my $funcname = $this->{escapefunc}; #push(@$bufref, "push(\@_buf, $funcname($expr)); "); push(@$bufref, "$funcname($expr), "); #push(@$bufref, $funcname, "(", $expr, "), "); } else { #push(@$bufref, "push(\@_buf, $expr); "); push(@$bufref, "$expr, "); #push(@$bufref, $expr, ", "); } } sub defun { ## (experimental) my $this = shift; my $funcname = shift; my @args = @_; if (! $funcname) { $_ = $this->{filename}; s/\.\w+$// if ($_); s/[^\w]/_/g if ($_); $funcname = "render_" . $_; } my @buf = (); push(@buf, "sub $funcname {"); push(@buf, " my (\$_context) = \@_; "); for my $arg (@args) { push(@buf, "my \$$arg = \$_context->{'$arg'}; "); } push(@buf, $this->{script}); push(@buf, "}\n"); return join('', @buf); } ## compile $this->{script} into closure. sub compile { my $this = shift; if ($this->{args}) { #my $f = $Tenjin::CONTEXT_CLASS . '::to_func'; #my $func = $f->($this->{script}); my $func = $Tenjin::CONTEXT_CLASS->to_func($this->{script}); $@ and die("*** Error: " . $this->{filename} . "\n", $@); return $this->{func} = $func; } return; } ## ## preprocessor ## package Tenjin::Preprocessor; #@ISA = ('Tenjin::Template'); @Tenjin::Preprocessor::ISA = ('Tenjin::Template'); my $STMT_PATTERN = Tenjin::Template::compile_stmt_pattern('PL'); sub stmt_pattern { my $this = shift; return $STMT_PATTERN; } my $EXPR_PATTERN = qr/\[\*=(=?)(.*?)(=?)=\*\]/s; sub expr_pattern { my $this = shift; return $EXPR_PATTERN; } sub add_expr { my $this = shift; my ($bufref, $expr, $flag_escape) = @_; $expr = "Tenjin::Util::_decode_params($expr)"; $this->SUPER::add_expr($bufref, $expr, $flag_escape); } ## ## engine class which handles several template objects. ## ## ex. ## use Tenjin; ## $Tenjin::USE_STRICT = 1; ## optional ## my $engine = new Tenjin::Engine({'layout'=>'layout.plhtml'}); ## my $context = { 'title'=>'Example', 'items'=>['A','B','C'], }; ## print $engine->render('example.plhtml', $context); ## package Tenjin::Engine; sub new { my $class = shift; my ($options) = @_; my $this = {}; for my $key (qw[prefix postfix layout path cache preprocess templateclass]) { $this->{$key} = delete($options->{$key}); #$this->{$key} = $options->{$key}; } $this->{cache} = 1 unless defined($this->{cache}); $this->{init_opts_for_template} = $options; $this->{templates} = {}; $this->{prefix} = '' if (! $this->{prefix}); $this->{postfix} = '' if (! $this->{postfix}); return bless($this, $class); } sub to_filename { my $this = shift; my ($template_name) = @_; if (substr($template_name, 0, 1) eq ':') { return $this->{prefix} . substr($template_name, 1) . $this->{postfix}; } return $template_name; } sub find_template_file { my $this = shift; my ($filename) = @_; my $path = $this->{path}; if ($path) { my $sep = $^O eq 'MSWin32' ? '\\\\' : '/'; for my $dirname (@$path) { my $filepath = $dirname . $sep . $filename; return $filepath if (-f $filepath); } } else { return $filename if (-f $filename); } my $s = $path ? ("['" . join("','", @$path) . "']") : '[]'; die "$filename: not found. (path=$s)"; } sub register_template { my $this = shift; my ($template_name, $template) = @_; $this->{templates}->{$template_name} = $template; } sub get_template { my $this = shift; my ($template_name, $_context) = @_; my $template = $this->{templates}->{$template_name}; my $t = $template; if (! $t || $t->{timestamp} && $t->{filename} && $t->{timestamp} < _mtime($t->{filename})) { my $filename = $this->to_filename($template_name); my $filepath = $this->find_template_file($filename); $template = $this->create_template($filepath, $_context); # $_context is passed only for preprocessor $this->register_template($template_name, $template); } return $template; } sub read_template_file { my $this = shift; my ($template, $filename, $_context) = @_; my $input; if ($this->{preprocess}) { if (! defined($_context) || ! $_context->{_engine}) { $_context = {}; $this->hook_context($context); } $input = (new Tenjin::Preprocessor($filename))->render($_context); } else { $input = Tenjin::Util::read_file($filename, 1); } return $input; } sub store_cachefile { my $this = shift; my ($cachename, $template) = @_; my $cache = $template->{script}; if (defined($template->{args})) { my $args = $template->{args}; $cache = "\#\@ARGS " . join(',', @$args) . "\n" . $cache; } Tenjin::Util::write_file($cachename, $cache, 1); } sub load_cachefile { my $this = shift; my ($cachename, $template) = @_; my $cache = Tenjin::Util::read_file($cachename, 1); if ($cache =~ s/\A\#\@ARGS (.*)\r?\n//) { my $argstr = $1; $argstr =~ s/\A\s+|\s+\Z//g; my @args = split(',', $argstr); $template->{args} = \@args; } $template->{script} = $cache; } sub cachename { my $this = shift; my ($filename) = @_; return $filename . '.cache'; } sub create_template { my $this = shift; my ($filename, $_context) = @_; my $cachename = $this->cachename($filename); my $klass = $this->{templateclass} || $Tenjin::TEMPLATE_CLASS; # Tenjin::Template; my $template = $klass->new(undef, $this->{init_opts_for_template}); $template->{timestamp} = time(); if (! $this->{cache}) { #print STDERR "*** debug: caching is off.\n"; my $input = $this->read_template_file($template, $filename, $_context); $template->convert($input, $filename); } elsif (!(-f $cachename) || ((-f $filename) && _mtime($cachename) < _mtime($filename)) ) { #print STDERR "*** debug: $cachename: cache file is not found or old.\n"; my $input = $this->read_template_file($template, $filename, $_context); $template->convert($input, $filename); $this->store_cachefile($cachename, $template); } else { #print STDERR "*** debug: $cachename: cache file is found.\n"; $template->{filename} = $filename; $this->load_cachefile($cachename, $template); } $template->compile(); return $template; } sub _mtime { my ($filename) = @_; return (stat($filename))[9]; } sub _render { my $this = shift; my ($template_name, $context, $layout) = @_; $context = {} unless defined($context); $layout = 1 unless defined($layout); $this->hook_context($context); my $output; while (1) { my $template = $this->get_template($template_name, $context); # pass $context only for preprocessing $output = $template->_render($context); return $template->{filename} if ($@); # return template filename when error happened $layout = $context->{_layout} if exists($context->{_layout}); $layout = $this->{layout} if $layout == 1; last unless $layout; $template_name = $layout; $layout = undef; $context->{_content} = $output; delete($context->{_layout}); } return $output; } sub render { my $this = shift; my $ret = $this->_render(@_); if ($@) { # error happened my $template_filename = $ret; die "*** ERROR: $template_filename\n", $@; } my $output = $ret; return $output; } sub hook_context { my $this = shift; my ($context) = @_; $context->{_engine} = $this; } 1;