diff --git a/template.skel.pm b/template.skel.pm
new file mode 100644
index 0000000..fc6bd16
--- /dev/null
+++ b/template.skel.pm
@@ -0,0 +1,945 @@
+####################################################################
+#
+# ANY CHANGE MADE HERE WILL BE LOST !
+#
+# This file was generated using Parse::Yapp version <<$version>>.
+# Don't edit this file, edit template.skel.pm and template.yp instead.
+#
+####################################################################
+
+package VMXTemplate::Parser;
+
+use strict;
+use base qw(Parse::Yapp::Driver);
+<<$driver>>
+
+VMXTemplate::Utils::import();
+
+<<$head>>
+sub new
+{
+ my $class = shift;
+ $class = ref($class) || $class;
+ return bless $class->SUPER::new(
+ yyversion => '<<$version>>',
+ yystates =>
+<<$states>>,
+ yyrules =>
+<<$rules>>,
+#line 30 "template.skel.pm"
+ @_
+ ), $class;
+}
+
+sub _Lexer
+{
+ my ($parser) = shift;
+ return $parser->{__lexer}->read_token;
+}
+
+sub _error
+{
+ my ($self) = @_;
+ $self->{__lexer}->warn('Unexpected ' . $self->YYCurtok . ($self->YYCurval ? ' ' . $self->YYCurval : ''));
+ $self->{__lexer}->skip_error;
+}
+
+sub compile
+{
+ my ($self, $text) = @_;
+ $self->{__lexer} ||= new VMXTemplate::Lexer($self, $self->{__options});
+ $self->{__lexer}->set_code($text);
+ $self->YYParse(yylex => \&_Lexer, yyerror => \&_error);
+}
+
+package VMXTemplate::Lexer;
+
+# Possible tokens consisting of special characters
+my $chartokens = '+ - = * / % ! , . < > ( ) { } [ ] & .. || && == != <= >= =>';
+
+# Reserved keywords
+my $keywords_str = 'OR XOR AND NOT IF ELSE ELSIF ELSEIF END SET FOR FOREACH FUNCTION BLOCK MACRO';
+
+sub new
+{
+ my $class = shift;
+ $class = ref($class) || $class;
+ my ($options) = @_;
+
+ my $self = bless {
+ options => $options,
+
+ # Input
+ code => '',
+ eaten => '',
+ lineno => 0,
+
+ # Preprocessed keyword tokens
+ nchar => {},
+ lens => [],
+ keywords => { map { $_ => 1 } split / /, $keywords_str },
+
+ # Last directive start position, directive and substitution start/end counters
+ last_start => 0,
+ last_start_line => 0,
+ in_code => 0,
+ in_subst => 0,
+ }, $class;
+
+ foreach (split(/ /, $chartokens))
+ {
+ $self->{nchar}{length($_)}{$_} = 1;
+ }
+ # Add code fragment finishing tokens
+ $self->{nchar}{length($self->{options}->{end_code})}{$self->{options}->{end_code}} = 1;
+ if ($self->{options}->{end_subst})
+ {
+ $self->{nchar}{length($self->{options}->{end_subst})}{$self->{options}->{end_subst}} = 1;
+ }
+ # Reverse-sort lengths
+ $self->{lens} = [ sort { $b <=> $a } keys %{$self->{nchar}} ];
+
+ return $self;
+}
+
+sub set_code
+{
+ my $self = shift;
+ my ($code) = @_;
+ $self->{code} = $code;
+ $self->{eaten} = '';
+ $self->{lineno} = $self->{in_code} = $self->{in_subst} = 0;
+ $self->{last_start} = $self->{last_start_line} = 0;
+}
+
+sub eat
+{
+ my $self = shift;
+ my ($len) = @_;
+ my $str = substr($self->{code}, 0, $len, '');
+ $self->{eaten} .= $str;
+ $self->{lineno} += ($str =~ tr/\n/\n/);
+ return $str;
+}
+
+sub skip_error
+{
+ my ($self) = @_;
+ $self->{code} = substr($self->{eaten}, $self->{last_start}+1, length($self->{eaten}), '') . $self->{code};
+ $self->{lineno} = $self->{last_start_line};
+ $self->{in_code} = $self->{in_subst} = 0;
+}
+
+sub read_token
+{
+ my $self = shift;
+ if (!length $self->{code})
+ {
+ # End of code
+ return;
+ }
+ if ($self->{in_code} <= 0 && $self->{in_subst} <= 0)
+ {
+ my $r;
+ my $code_pos = index($self->{code}, $self->{options}->{begin_code});
+ my $subst_pos = index($self->{code}, $self->{options}->{begin_subst});
+ if ($code_pos == -1 && $subst_pos == -1)
+ {
+ # No more directives
+ $r = [ 'literal', [ "'".addcslashes($self->eat(length $self->{code}), "'\\")."'", 1 ] ];
+ }
+ elsif ($subst_pos == -1 || $code_pos >= 0 && $subst_pos > $code_pos)
+ {
+ # Code starts closer
+ if ($code_pos > 0)
+ {
+ # We didn't yet reach the code beginning
+ my $str = $self->eat($code_pos);
+ if ($self->{options}->{eat_code_line})
+ {
+ $str =~ s/\n[ \t]*$/\n/s;
+ }
+ $r = [ 'literal', [ "'".addcslashes($str, "'\\")."'", 1 ] ];
+ }
+ else
+ {
+ # We are at the code beginning
+ my $i = length $self->{options}->{begin_code};
+ if ($self->{code} =~ /^.{$i}([ \t]+)/s)
+ {
+ $i += length $1;
+ }
+ if ($i < length($self->{code}) && substr($self->{code}, $i, 1) eq '#')
+ {
+ # Strip comment and retry
+ $i = index($self->{code}, $self->{options}->{end_code}, $i);
+ $i = $i >= 0 ? $i+length($self->{options}->{end_code}) : length $self->{code};
+ $self->eat($i);
+ return $self->read_token();
+ }
+ $r = [ '', $t);
+ }
+ }
+ elsif ($self->{in_subst})
+ {
+ $self->{in_subst}++ if $t eq $self->{options}->{begin_subst};
+ $self->{in_subst}-- if $t eq $self->{options}->{end_subst};
+ if (!$self->{in_subst})
+ {
+ return ('}}', $t);
+ }
+ }
+ return ($t, undef);
+ }
+ }
+ # Unknown character
+ $self->warn("Unexpected character '".substr($self->{code}, 0, 1)."'");
+ return ('error', undef);
+ }
+}
+
+sub errorinfo
+{
+ my $self = shift;
+ my $linestart = rindex($self->{eaten}, "\n");
+ my $lineend = index($self->{code}, "\n");
+ $lineend = length($self->{code}) if $lineend < 0;
+ my $line = substr($self->{eaten}, $linestart+1) . '^^^' . substr($self->{code}, 0, $lineend);
+ my $charpos;
+ {
+ use bytes;
+ $charpos = length $self->{eaten};
+ }
+ return ' in '.$self->{options}->{input_filename}.', line '.($self->{lineno}+1).
+ ', character '.$charpos.', marked by ^^^ in '.$line;
+}
+
+sub warn
+{
+ my $self = shift;
+ my ($text) = @_;
+ $self->{options}->error($text.$self->errorinfo());
+}
+
+package VMXTemplate::Utils;
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ TS_UNIX TS_DB TS_DB_DATE TS_MW TS_EXIF TS_ORACLE TS_ISO_8601 TS_RFC822
+ timestamp plural_ru strlimit strip_tags addcslashes requote
+);
+
+use constant {
+ TS_UNIX => 0,
+ TS_DB => 1,
+ TS_DB_DATE => 2,
+ TS_MW => 3,
+ TS_EXIF => 4,
+ TS_ORACLE => 5,
+ TS_ISO_8601 => 6,
+ TS_RFC822 => 7,
+};
+
+my @Mon = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
+my %mon = qw(jan 0 feb 1 mar 2 apr 3 may 4 jun 5 jul 6 aug 7 sep 8 oct 9 nov 10 dec 11);
+my @Wday = qw(Sun Mon Tue Wed Thu Fri Sat);
+
+our $safe_tags = 'div|blockquote|span|a|b|i|u|p|h1|h2|h3|h4|h5|h6|strike|strong|small|big|blink|center|ol|pre|sub|sup|font|br|table|tr|td|th|tbody|tfoot|thead|tt|ul|li|em|img|marquee|cite';
+
+# ограниченная распознавалка дат
+sub timestamp
+{
+ my ($ts, $format) = @_;
+
+ require POSIX;
+ if (int($ts) eq $ts)
+ {
+ # TS_UNIX or Epoch
+ $ts = time if !$ts;
+ }
+
+ elsif ($ts =~ /^\D*(\d{4,}?)\D*(\d{2})\D*(\d{2})\D*(?:(\d{2})\D*(\d{2})\D*(\d{2})\D*([\+\- ]\d{2}\D*)?)?$/so)
+ {
+ # TS_DB, TS_DB_DATE, TS_MW, TS_EXIF, TS_ISO_8601
+ $ts = POSIX::mktime($6||0, $5||0, $4||0, $3, $2-1, $1-1900);
+ }
+ elsif ($ts =~ /^\s*(\d\d?)-(...)-(\d\d(?:\d\d)?)\s*(\d\d)\.(\d\d)\.(\d\d)/so)
+ {
+ # TS_ORACLE
+ $ts = POSIX::mktime($6, $5, $4, int($1), $mon{lc $2}, $3 < 100 ? $3 : $3-1900);
+ }
+ elsif ($ts =~ /^\s*..., (\d\d?) (...) (\d{4,}) (\d\d):(\d\d):(\d\d)\s*([\+\- ]\d\d)\s*$/so)
+ {
+ # TS_RFC822
+ $ts = POSIX::mktime($6, $5, $4, int($1), $mon{lc $2}, $3-1900);
+ }
+ else
+ {
+ # Bogus value, return undef
+ return undef;
+ }
+
+ if (!$format)
+ {
+ # TS_UNIX
+ return $ts;
+ }
+ elsif ($format == TS_MW)
+ {
+ return POSIX::strftime("%Y%m%d%H%M%S", localtime($ts));
+ }
+ elsif ($format == TS_DB)
+ {
+ return POSIX::strftime("%Y-%m-%d %H:%M:%S", localtime($ts));
+ }
+ elsif ($format == TS_DB_DATE)
+ {
+ return POSIX::strftime("%Y-%m-%d", localtime($ts));
+ }
+ elsif ($format == TS_ISO_8601)
+ {
+ return POSIX::strftime("%Y-%m-%dT%H:%M:%SZ", localtime($ts));
+ }
+ elsif ($format == TS_EXIF)
+ {
+ return POSIX::strftime("%Y:%m:%d %H:%M:%S", localtime($ts));
+ }
+ elsif ($format == TS_RFC822)
+ {
+ my @l = localtime($ts);
+ return POSIX::strftime($Wday[$l[6]].", %d ".$Mon[$l[4]]." %Y %H:%M:%S %z", @l);
+ }
+ elsif ($format == TS_ORACLE)
+ {
+ my @l = localtime($ts);
+ return POSIX::strftime("%d-".$Mon[$l[4]]."-%Y %H.%M.%S %p", @l);
+ }
+ return $ts;
+}
+
+# Select one of 3 plural forms for russian language
+sub plural_ru
+{
+ my ($count, $one, $few, $many) = @_;
+ my $sto = $count % 100;
+ if ($sto >= 10 && $sto <= 20)
+ {
+ return $many;
+ }
+ my $r = $count % 10;
+ if ($r == 1)
+ {
+ return $one;
+ }
+ elsif ($r >= 2 && $r <= 4)
+ {
+ return $few;
+ }
+ return $many;
+}
+
+# Limit string to $maxlen
+sub strlimit
+{
+ my ($str, $maxlen, $dots) = @_;
+ if (!$maxlen || $maxlen < 1 || length($str) <= $maxlen)
+ {
+ return $str;
+ }
+ $str = substr($str, 0, $maxlen);
+ my $p = rindex($str, ' ');
+ if ($p < 0 || (my $pt = rindex($str, "\t")) > $p)
+ {
+ $p = $pt;
+ }
+ if ($p > 0)
+ {
+ $str = substr($str, 0, $p);
+ }
+ return $str . (defined $dots ? $dots : '...');
+}
+
+# Escape HTML special chars
+sub htmlspecialchars
+{
+ local $_ = $_[0];
+ s/&/&/gso;
+ s/</gso;
+ s/>/>/gso;
+ s/\"/"/gso;
+ s/\'/'/gso;
+ return $_;
+}
+
+# Replace (some) tags with whitespace
+sub strip_tags
+{
+ my ($str, $allowed) = @_;
+ my $allowed = $allowed ? '(?!/?('.$allowed.'))' : '';
+ $str =~ s/(<$allowed\/?[a-z][a-z0-9-]*(\s+[^<>]*)?>\s*)+/ /gis;
+ return $str;
+}
+
+# Strip unsafe tags
+sub strip_unsafe_tags
+{
+ return strip_tags($_[0], $safe_tags);
+}
+
+# Add '\' before specified chars
+sub addcslashes
+{
+ my ($str, $escape) = @_;
+ $str =~ s/([$escape])/\\$1/gs;
+ return $str;
+}
+
+# Quote regexp-special characters in $_[0]
+sub requote
+{
+ "\Q$_[0]\E";
+}
+
+# Escape quotes in C style, also \n and \r
+sub quotequote
+{
+ my ($a) = @_;
+ $a =~ s/[\\\'\"]/\\$&/gso;
+ $a =~ s/\n/\\n/gso;
+ $a =~ s/\r/\\r/gso;
+ return $a;
+}
+
+# Escape quotes in SQL or CSV style (" --> "")
+sub sql_quote
+{
+ my ($a) = @_;
+ $a =~ s/\"/\"\"/gso;
+ return $a;
+}
+
+# Replace regular expression, returning result
+sub regex_replace
+{
+ my ($re, $repl, $s) = @_;
+ $re = qr/$re/s if !ref $re;
+ # Escape \ @ $ % /, but allow $n replacements ($1 $2 $3 ...)
+ $repl =~ s!([\\\@\%/]|\$(?\!\d))!\\$1!gso;
+ eval("\$s =~ s/\$re/$repl/gs");
+ return $s;
+}
+
+# Replace strings
+sub str_replace
+{
+ my ($str, $repl, $s) = @_;
+ $s =~ s/\Q$str\E/$repl/gs;
+ return $s;
+}
+
+# extract elements from array
+# array_slice([], 0, 10)
+# array_slice([], 2)
+# array_slice([], 0, -1)
+sub array_slice
+{
+ my ($array, $from, $to) = @_;
+ return $array unless $from;
+ $to ||= 0;
+ $from += @$array if $from < 0;
+ $to += @$array if $to <= 0;
+ return [ @$array[$from..$to] ];
+}
+
+# extract each $div'th element from array, starting with $mod
+# array_div([], 2)
+# array_div([], 2, 1)
+sub array_div
+{
+ my ($array, $div, $mod) = @_;
+ return $array unless $div;
+ $mod ||= 0;
+ return [ @$array[grep { $_ % $div == $mod } 0..$#$array] ];
+}
+
+# JSON encoding
+sub encode_json
+{
+ require JSON;
+ *encode_json = *JSON::encode_json;
+ goto &JSON::encode_json;
+}
+
+package VMXTemplate::Compiler;
+
+# function subst()
+sub exec_subst
+{
+ my $str = shift;
+ $str =~ s/(? ..., value => ... }, ... ]
+sub exec_pairs
+{
+ my $hash = shift;
+ return [ map { { key => $_, value => $hash->{$_} } } sort keys %{ $hash || {} } ];
+}
+
+# check if the argument is an arrayref
+sub exec_is_array
+{
+ return ref $_[1] && $_[1] =~ /ARRAY/;
+}
+
+# get array or hash element
+sub exec_get
+{
+ defined $_[1] && ref $_[0] || return $_[0];
+ $_[0] =~ /ARRAY/ && return $_[0]->[$_[1]];
+ return $_[0]->{$_[1]};
+}
+
+# type-dependent comparison
+sub exec_cmp
+{
+ my ($a, $b) = @_;
+ my $n = grep /^-?\d+(\.\d+)?$/, $a, $b;
+ return $n ? $a <=> $b : $a cmp $b;
+}
+
+# Function aliases
+my $functions = {
+ 'i' => 'int',
+ 'intval' => 'int',
+ 'lower' => 'lc',
+ 'lowercase' => 'lc',
+ 'upper' => 'uc',
+ 'uppercase' => 'uc',
+ 'addslashes' => 'quote',
+ 'q' => 'quote',
+ 're_quote' => 'requote',
+ 'preg_quote' => 'requote',
+ 'uri_escape' => 'urlencode',
+ 'uriquote' => 'urlencode',
+ 'substring' => 'substr',
+ 'htmlspecialchars' => 'html',
+ 's' => 'html',
+ 'strip_tags' => 'strip',
+ 't' => 'strip',
+ 'h' => 'strip_unsafe',
+ 'sq' => 'sql_quote',
+ 'implode' => 'join',
+ 'truncate' => 'strlimit',
+ 'hash_keys' => 'keys',
+ 'array_keys' => 'keys',
+ 'array_slice' => 'subarray',
+ 'hget' => 'get',
+ 'aget' => 'get',
+ 'var_dump' => 'dump',
+ 'process' => 'parse',
+ 'include' => 'parse',
+ 'process_inline' => 'parse_inline',
+ 'include_inline' => 'parse_inline',
+ 'subarray' => 'array_slice',
+ 'subarray_divmod' => 'array_div',
+};
+
+# Functions that do escape HTML, for safe mode
+use constant Q_ALWAYS => -1;
+use constant Q_IF_ALL => -2;
+use constant Q_ALL_BUT_FIRST => -3;
+use constant Q_ALWAYS_NUM => -4;
+
+my $functionSafeness = {
+ 'int' => Q_ALWAYS_NUM,
+ 'raw' => Q_ALWAYS,
+ 'html' => Q_ALWAYS,
+ 'strip' => Q_ALWAYS,
+ 'strip_unsafe' => Q_ALWAYS,
+ 'parse' => Q_ALWAYS,
+ 'parse_inline' => Q_ALWAYS,
+ 'exec' => Q_ALWAYS,
+ 'exec_from' => Q_ALWAYS,
+ 'exec_from_inline' => Q_ALWAYS,
+ 'quote' => Q_ALWAYS,
+ 'sql_quote' => Q_ALWAYS,
+ 'requote' => Q_ALWAYS,
+ 'urlencode' => Q_ALWAYS,
+ 'and' => Q_ALWAYS,
+ 'or' => Q_IF_ALL,
+ 'not' => Q_ALWAYS_NUM,
+ 'add' => Q_ALWAYS_NUM,
+ 'sub' => Q_ALWAYS_NUM,
+ 'mul' => Q_ALWAYS_NUM,
+ 'div' => Q_ALWAYS_NUM,
+ 'mod' => Q_ALWAYS_NUM,
+ 'log' => Q_ALWAYS_NUM,
+ 'even' => Q_ALWAYS_NUM,
+ 'odd' => Q_ALWAYS_NUM,
+ 'eq' => Q_ALWAYS_NUM,
+ 'ne' => Q_ALWAYS_NUM,
+ 'gt' => Q_ALWAYS_NUM,
+ 'lt' => Q_ALWAYS_NUM,
+ 'ge' => Q_ALWAYS_NUM,
+ 'le' => Q_ALWAYS_NUM,
+ 'seq' => Q_ALWAYS_NUM,
+ 'sne' => Q_ALWAYS_NUM,
+ 'sgt' => Q_ALWAYS_NUM,
+ 'slt' => Q_ALWAYS_NUM,
+ 'sge' => Q_ALWAYS_NUM,
+ 'sle' => Q_ALWAYS_NUM,
+ 'neq' => Q_ALWAYS_NUM,
+ 'nne' => Q_ALWAYS_NUM,
+ 'ngt' => Q_ALWAYS_NUM,
+ 'nlt' => Q_ALWAYS_NUM,
+ 'nge' => Q_ALWAYS_NUM,
+ 'nle' => Q_ALWAYS_NUM,
+ 'strlen' => Q_ALWAYS_NUM,
+ 'strftime' => Q_ALWAYS,
+ 'str_replace' => Q_ALL_BUT_FIRST,
+ 'substr' => 1, # parameter number to take safeness from
+ 'trim' => 1,
+ 'split' => 1,
+ 'nl2br' => 1,
+ 'concat' => Q_IF_ALL,
+ 'join' => Q_IF_ALL,
+ 'subst' => Q_IF_ALL,
+ 'strlimit' => 1,
+ 'plural_ru' => Q_ALL_BUT_FIRST,
+ 'hash' => Q_IF_ALL,
+ 'keys' => 1,
+ 'values' => 1,
+ 'sort' => 1,
+ 'pairs' => 1,
+ 'array' => Q_IF_ALL,
+ 'range' => Q_ALWAYS,
+ 'is_array' => Q_ALWAYS_NUM,
+ 'count' => Q_ALWAYS_NUM,
+ 'array_slice' => 1,
+ 'array_div' => 1,
+ 'set' => 2,
+ 'array_merge' => Q_IF_ALL,
+ 'shift' => 1,
+ 'pop' => 1,
+ 'unshift' => Q_ALWAYS,
+ 'push' => Q_ALWAYS,
+ 'void' => Q_ALWAYS,
+ 'json' => Q_ALWAYS,
+ 'map' => Q_ALL_BUT_FIRST,
+ 'yesno' => Q_ALL_BUT_FIRST,
+};
+
+sub fmop
+{
+ my $op = shift;
+ return "((" . join(") $op (", @_) . "))";
+}
+
+# логические операции
+sub function_or { fmop('||', @_) }
+sub function_and { fmop('&&', @_) }
+sub function_not { "!($_[1])" }
+# арифметические операции
+sub function_add { fmop('+', @_) }
+sub function_sub { fmop('-', @_) }
+sub function_mul { fmop('*', @_) }
+sub function_div { fmop('/', @_) }
+sub function_mod { fmop('%', @_) }
+# логарифм
+sub function_log { "log($_[1])" }
+# чётный, нечётный
+sub function_even { "!(($_[1]) & 1)" }
+sub function_odd { "(($_[1]) & 1)" }
+# приведение к целому числу
+sub function_int { "int($_[1])" }
+# сравнения: = != > < >= <= (типозависимые)
+sub function_eq { "(exec_cmp($_[1], $_[2]) == 0)" }
+sub function_ne { "(exec_cmp($_[1], $_[2]) != 0)" }
+sub function_gt { "(exec_cmp($_[1], $_[2]) > 0)" }
+sub function_lt { "(exec_cmp($_[1], $_[2]) < 0)" }
+sub function_ge { "(exec_cmp($_[1], $_[2]) >= 0)" }
+sub function_le { "(exec_cmp($_[1], $_[2]) <= 0)" }
+# сравнения: = != > < >= <= (строковые)
+sub function_seq { "(($_[1]) eq ($_[2]))" }
+sub function_sne { "(($_[1]) ne ($_[2]))" }
+sub function_sgt { "(($_[1]) gt ($_[2]))" }
+sub function_slt { "(($_[1]) lt ($_[2]))" }
+sub function_sge { "(($_[1]) ge ($_[2]))" }
+sub function_sle { "(($_[1]) le ($_[2]))" }
+# сравнения: = != > < >= <= (численные)
+sub function_neq { "(($_[1]) == ($_[2]))" }
+sub function_nne { "(($_[1]) != ($_[2]))" }
+sub function_ngt { "(($_[1]) > ($_[2]))" }
+sub function_nlt { "(($_[1]) < ($_[2]))" }
+sub function_nge { "(($_[1]) >= ($_[2]))" }
+sub function_nle { "(($_[1]) <= ($_[2]))" }
+# тернарный оператор $1 ? $2 : $3
+sub function_yesno { "(($_[1]) ? ($_[2]) : ($_[3]))" }
+
+## Строки
+
+# нижний и верхний регистр
+sub function_lc { "lc($_[1])" }
+sub function_uc { "uc($_[1])" }
+# нижний и верхний регистр первого символа
+sub function_lcfirst { "lcfirst($_[1])" }
+sub function_ucfirst { "ucfirst($_[1])" }
+# экранировать двойные и одинарные кавычки в стиле C (добавить \)
+sub function_quote { "quotequote($_[1])" }
+# экранировать двойные кавычки в стиле SQL/CSV (удвоением)
+sub function_sql_quote { "sql_quote($_[1])" }
+# экранирование символов, специальных для регулярного выражения
+sub function_requote { "requote($_[1])" }
+# кодировать символы в стиле URL
+sub function_urlencode { shift; "URI::Escape::uri_escape(".join(",",@_).")" }
+# декодировать символы в стиле URL
+sub function_urldecode { shift; "URI::Escape::uri_unescape(".join(",",@_).")" }
+# замена регэкспов
+sub function_replace { "regex_replace($_[1], $_[2], $_[3])" }
+# замена подстрок (а не регэкспов)
+sub function_str_replace { "str_replace($_[1], $_[2], $_[3])" }
+# длина строки в символах
+sub function_strlen { "strlen($_[1])" }
+# подстрока
+sub function_substr { shift; "substr(".join(",", @_).")" }
+# обрезать пробелы из начала и конца строки
+sub function_trim { shift; "trim($_[0])" }
+# разделить строку $2 по регулярному выражению $1 опционально с лимитом $3
+sub function_split { shift; "split(".join(",", @_).")" }
+# заменить символы & < > " ' на HTML-сущности
+sub function_html { "htmlspecialchars($_[1])" }
+# удалить все HTML-теги
+sub function_strip { "strip_tags($_[1])" }
+# оставить только "безопасные" HTML-теги
+sub function_strip_unsafe { "strip_unsafe_tags($_[1])" }
+# заменить \n на
+sub function_nl2br { "regex_replace(qr/\\n/s, '
', $_[1])" }
+# конкатенация строк
+sub function_concat { fmop('.', @_) }
+# объединяет не просто скаляры, а также все элементы массивов
+sub function_join { fearr('join', 1, @_) }
+# подставляет на места $1, $2 и т.п. в строке аргументы
+sub function_subst { fearr('exec_subst', 1, @_) }
+# sprintf
+sub function_sprintf { fearr('sprintf', 1, @_) }
+# strftime
+sub function_strftime
+{
+ my $self = shift;
+ my ($fmt, $date, $time) = @_;
+ $date = "($date).' '.($time)" if $time;
+ $date = "POSIX::strftime($date, localtime(timestamp($date)))";
+ $date = "utf8on($date)" if $self->{use_utf8};
+ return $date;
+}
+# ограничение длины строки $maxlen символами на границе пробелов и добавление '...', если что.
+sub function_strlimit { shift; "strlimit(".join(",", @_).")" }
+# выбор правильной формы множественного числа для русского языка
+sub function_plural_ru { shift; "plural_ru(".join(",", @_).")" }
+
+## Массивы и хеши
+
+# создание хеша
+sub function_hash { shift; @_ == 1 ? "{ \@{ $_[0] } }" : "{" . join(",", @_) . "}"; }
+# hash keys, values
+sub function_keys { '[ keys(%{'.$_[1].'}) ]'; }
+sub function_values { '[ values(%{'.$_[1].'}) ]'; }
+# сортировка массива
+sub function_sort { '[ '.fearr('sort', 0, @_).' ]'; }
+# пары { id => ключ, name => значение } для хеша
+sub function_pairs { "exec_pairs($_[1])" }
+# создание массива
+sub function_array { shift; "[" . join(",", @_) . "]"; }
+# диапазон значений
+sub function_range { "[ $_[1] .. $_[2] ]" }
+# проверка, аргумент - массив или не массив?
+sub function_is_array { "exec_is_array($_[1])" }
+# количество элементов _массива_ (не хеша)
+sub function_count { "(ref($_[1]) && $_[1] =~ /ARRAY/so ? scalar(\@{ $_[1] }) : 0)" }
+# подмассив по номерам элементов
+sub function_array_slice { shift; "array_slice(" . join(",", @_) . ")"; }
+# подмассив по кратности номеров элементов
+sub function_array_div { shift; "array_div(" . join(",", @_) . ")"; }
+# получить элемент хеша/массива по неконстантному ключу (например get(iteration.array, rand(5)))
+# по-моему, это лучше, чем Template Toolkit'овский ад - hash.key.${another.hash.key}.зюка.хрюка и т.п.
+sub function_get { shift; "exec_get(" . join(",", @_) . ")"; }
+# для хеша
+sub function_hget { "($_[1])->\{$_[2]}" }
+# для массива
+sub function_aget { "($_[1])->\[$_[2]]" }
+# присваивание (только lvalue)
+sub function_set { "scalar(($_[1] = $_[2]), '')" }
+# слияние массивов в один большой массив
+sub function_array_merge { shift; '[@{'.join('},@{',@_).'}]' }
+# вынуть первый элемент массива
+sub function_shift { "shift(\@{$_[1]})"; }
+# вынуть последний элемент массива
+sub function_pop { "pop(\@{$_[1]})"; }
+# вставить как первый элемент массива
+sub function_unshift { shift; "unshift(\@{".shift(@_)."}, ".join(",", @_).")"; }
+# вставить как последний элемент массива
+sub function_push { shift; "push(\@{".shift(@_)."}, ".join(",", @_).")"; }
+
+## Прочее
+
+# вычисление выражения и игнорирование результата, как в JS
+sub function_void { "scalar(($_[1]), '')" }
+# дамп переменной
+sub function_dump { shift; "exec_dump(" . join(",", @_) . ")" }
+# JSON-кодирование
+sub function_json { "encode_json($_[1])" }
+# return the value as is, to ignore automatic escaping of "unsafe" HTML
+sub function_raw { $_[1] }
+# apply the function to each array element
+sub function_map
+{
+ my $self = shift;
+ my $fn = shift;
+ if ($fn =~ /^[\"\'](\w+)[\"\']$/so)
+ {
+ return '(map { '.$self->compile_function($1, '$_').' } (@{'.join('}, @{', @_).'}))';
+ }
+ else
+ {
+ $self->{lexer}->warn("Non-constant function: unimplemented");
+ }
+}
+
+sub compile_function
+{
+ my $self = shift;
+ my ($fn, $args) = @_;
+ $fn = lc $fn;
+ if ($functions->{$fn})
+ {
+ # Function alias
+ $fn = $functions->{$fn};
+ }
+ # Calculate HTML safeness flag
+ my $q = $functionSafeness->{$fn};
+ if ($q > 0)
+ {
+ $q = exists $args->[$q-1] ? $args->[$q-1]->[1] : 1;
+ }
+ elsif ($q == Q_ALWAYS)
+ {
+ $q = 1;
+ }
+ elsif ($q == Q_ALWAYS_NUM)
+ {
+ $q = 'i';
+ }
+ else
+ {
+ $q = 1;
+ my $n = scalar @$args;
+ for (my $i = ($q == Q_ALL_BUT_FIRST ? 1 : 0); $i < $n; $i++)
+ {
+ $q = $q && $args->[$i]->[1];
+ }
+ }
+ my $argv = [ map { $_->[0] } @$args ];
+ my $r;
+ if ($self->can(my $ffn = "function_$fn"))
+ {
+ # Builtin function call using name
+ $r = $self->$ffn(@$argv);
+ }
+ elsif (my $ffn = $self->{options}->{compiletime_functions}->{$fn})
+ {
+ # Custom compile-time function call
+ $r = &$ffn($self, @$argv);
+ }
+ else
+ {
+ $self->{lexer}->warn("Unknown function: '$fn'");
+ $r = "''";
+ }
+ return [ $r, $q ];
+}
+
+1;
diff --git a/template.yp b/template.yp
index c8ccd14..39d7ac7 100644
--- a/template.yp
+++ b/template.yp
@@ -1,5 +1,7 @@
# Контекстно-свободная Parse::Yapp-грамматика шаблонизатора
#
+# Компилировать так: yapp -o VMXTemplate.pm -t template.skel.pm template.yp
+#
# {{ двойные скобки }} нужно исключительно чтобы маркеры начала и конца подстановки
# были уникальны в грамматике. Вместо них обычно используются { одинарные }, а
# выбор корректной лексемы - скобки или маркера - делает лексический анализатор.
@@ -13,11 +15,7 @@
# * Олдстайл BEGIN .. END ликвидирован
# * Возможно, нужно добавить в каком-то виде foreach ... as key => value
#
-# P.S: Комментарии типа "#{" и "#}" служат, чтобы тупой Parse::Yapp понимал парные скобки
-
-%{
-VMXTemplate::Utils::import();
-%}
+# P.S: Комментарии типа "#{" и "#}" служат, чтобы у тупого Parse::Yapp'а число скобок сходилось
%start template
@@ -82,7 +80,7 @@ chunks: {
}
;
chunk: literal {
- '$t .= ' . $_[1] . ";\n";
+ '$t .= ' . $_[1][0] . ";\n";
}
| '' {
$_[2];
@@ -210,22 +208,22 @@ exp: exp '..' exp {
[ '!$self->lt(' . $_[1][0] . ', ' . $_[3][0] . ')', 1 ];
}
| exp '+' exp {
- [ '(' . $_[1][0] . ' + ' . $_[3][0] . ')', 1 ];
+ [ '(' . $_[1][0] . ' + ' . $_[3][0] . ')', 'i' ];
}
| exp '-' exp {
- [ '(' . $_[1][0] . ' - ' . $_[3][0] . ')', 1 ];
+ [ '(' . $_[1][0] . ' - ' . $_[3][0] . ')', 'i' ];
}
| exp '&' exp {
- [ '(' . $_[1][0] . ' & ' . $_[3][0] . ')', 1 ];
+ [ '(' . $_[1][0] . ' & ' . $_[3][0] . ')', 'i' ];
}
| exp '*' exp {
- [ '(' . $_[1][0] . ' * ' . $_[3][0] . ')', 1 ];
+ [ '(' . $_[1][0] . ' * ' . $_[3][0] . ')', 'i' ];
}
| exp '/' exp {
- [ '(' . $_[1][0] . ' / ' . $_[3][0] . ')', 1 ];
+ [ '(' . $_[1][0] . ' / ' . $_[3][0] . ')', 'i' ];
}
| exp '%' exp {
- [ '(' . $_[1][0] . ' % ' . $_[3][0] . ')', 1 ];
+ [ '(' . $_[1][0] . ' % ' . $_[3][0] . ')', 'i' ];
}
| p10
;
@@ -248,9 +246,7 @@ p11: nonbrace
nonbrace: '{' hash '}' {
[ "{ " . $_[2] . " }", 1 ];
}
-| literal {
- [ $1, 1 ];
- }
+| literal
| varref
| name '(' ')' {
$_[0]->{template}->compile_function($_[1], []);
@@ -264,20 +260,6 @@ nonbrace: '{' hash '}' {
| name nonbrace {
$_[0]->{template}->compile_function($_[1], [ $_[3] ]);
}
-| method '(' ')' {
- [ $_[1].'()', 1 ];
- }
-| method '(' list ')' {
- my $argv = [];
- foreach my $a (@{$_[3]}) {
- push @$argv, $a->[0];
- }
- [ $_[1].'('.join(', ', @$argv).')', 1 ];
- }
-;
-method: varref '.' name {
- $_[1][0].'->'.$_[3];
- }
;
list: exp {
[ $_[1] ];
@@ -331,6 +313,12 @@ varpart: '.' name {
| '[' exp ']' {
($_[2][1] eq 'i' ? '['.$_[2][0].']' : "{".$_[2][0]."}");
}
+| '.' name '(' ')' {
+ '->'.$_[2].'()';
+ }
+| '.' name '(' list ')' {
+ '->'.$_[2].'('.join(', ', map { $_->[0] } @{$_[4]}).')';
+ }
;
varpath: {
'';
@@ -341,430 +329,3 @@ varpath: {
;
%%
-
-# Possible tokens consisting of special characters
-my $chartokens = '+ - = * / % ! , . < > ( ) { } [ ] & .. || && == != <= >= =>';
-
-# Reserved keywords
-my $keywords_str = 'OR XOR AND NOT IF ELSE ELSIF ELSEIF END SET FOR FOREACH FUNCTION BLOCK MACRO';
-
-sub _Lexer
-{
- my ($parser) = shift;
- return $parser->{__lexer}->read_token;
-}
-
-sub _error
-{
- my ($self) = @_;
- $self->{__lexer}->warn('Unexpected ' . $self->YYCurtok . ($self->YYCurval ? ' ' . $self->YYCurval : ''));
- $self->{__lexer}->skip_error;
-}
-
-sub compile
-{
- my ($self, $text) = @_;
- $self->{__lexer} ||= new VMXTemplate::Lexer($self, $self->{__options});
- $self->{__lexer}->set_code($text);
- $self->YYParse(yylex => \&_Lexer, yyerror => \&_error);
-}
-
-package VMXTemplate::Lexer;
-
-sub new
-{
- my $class = shift;
- $class = ref($class) || $class;
- my ($options) = @_;
-
- my $self = bless {
- options => $options,
-
- # Input
- code => '',
- eaten => '',
- lineno => 0,
-
- # Preprocessed keyword tokens
- nchar => {},
- lens => [],
- keywords => { map { $_ => 1 } split / /, $keywords_str },
-
- # Last directive start position, directive and substitution start/end counters
- last_start => 0,
- last_start_line => 0,
- in_code => 0,
- in_subst => 0,
- }, $class;
-
- foreach (split(/ /, $chartokens))
- {
- $self->{nchar}{length($_)}{$_} = 1;
- }
- # Add code fragment finishing tokens
- $self->{nchar}{length($self->{options}->{end_code})}{$self->{options}->{end_code}} = 1;
- if ($self->{options}->{end_subst})
- {
- $self->{nchar}{length($self->{options}->{end_subst})}{$self->{options}->{end_subst}} = 1;
- }
- # Reverse-sort lengths
- $self->{lens} = [ sort { $b <=> $a } keys %{$self->{nchar}} ];
-
- return $self;
-}
-
-sub set_code
-{
- my $self = shift;
- my ($code) = @_;
- $self->{code} = $code;
- $self->{eaten} = '';
- $self->{lineno} = $self->{in_code} = $self->{in_subst} = 0;
- $self->{last_start} = $self->{last_start_line} = 0;
-}
-
-sub eat
-{
- my $self = shift;
- my ($len) = @_;
- my $str = substr($self->{code}, 0, $len, '');
- $self->{eaten} .= $str;
- $self->{lineno} += ($str =~ tr/\n/\n/);
- return $str;
-}
-
-sub skip_error
-{
- my ($self) = @_;
- $self->{code} = substr($self->{eaten}, $self->{last_start}+1, length($self->{eaten}), '') . $self->{code};
- $self->{lineno} = $self->{last_start_line};
- $self->{in_code} = $self->{in_subst} = 0;
-}
-
-sub read_token
-{
- my $self = shift;
- if (!length $self->{code})
- {
- # End of code
- return;
- }
- if ($self->{in_code} <= 0 && $self->{in_subst} <= 0)
- {
- my $r;
- my $code_pos = index($self->{code}, $self->{options}->{begin_code});
- my $subst_pos = index($self->{code}, $self->{options}->{begin_subst});
- if ($code_pos == -1 && $subst_pos == -1)
- {
- # No more directives
- $r = [ 'literal', "'".addcslashes($self->eat(length $self->{code}), "'\\")."'" ];
- }
- elsif ($subst_pos == -1 || $code_pos >= 0 && $subst_pos > $code_pos)
- {
- # Code starts closer
- if ($code_pos > 0)
- {
- # We didn't yet reach the code beginning
- my $str = $self->eat($code_pos);
- if ($self->{options}->{eat_code_line})
- {
- $str =~ s/\n[ \t]*$/\n/s;
- }
- $r = [ 'literal', "'".addcslashes($str, "'\\")."'" ];
- }
- else
- {
- # We are at the code beginning
- my $i = length $self->{options}->{begin_code};
- if ($self->{code} =~ /^.{$i}([ \t]+)/s)
- {
- $i += length $1;
- }
- if ($i < length($self->{code}) && substr($self->{code}, $i, 1) eq '#')
- {
- # Strip comment and retry
- $i = index($self->{code}, $self->{options}->{end_code}, $i);
- $i = $i >= 0 ? $i+length($self->{options}->{end_code}) : length $self->{code};
- $self->eat($i);
- return $self->read_token();
- }
- $r = [ '', $t);
- }
- }
- elsif ($self->{in_subst})
- {
- $self->{in_subst}++ if $t eq $self->{options}->{begin_subst};
- $self->{in_subst}-- if $t eq $self->{options}->{end_subst};
- if (!$self->{in_subst})
- {
- return ('}}', $t);
- }
- }
- return ($t, undef);
- }
- }
- # Unknown character
- $self->warn("Unexpected character '".substr($self->{code}, 0, 1)."'");
- return ('error', undef);
- }
-}
-
-sub errorinfo
-{
- my $self = shift;
- my $linestart = rindex($self->{eaten}, "\n");
- my $lineend = index($self->{code}, "\n");
- $lineend = length($self->{code}) if $lineend < 0;
- my $line = substr($self->{eaten}, $linestart+1) . '^^^' . substr($self->{code}, 0, $lineend);
- my $charpos;
- {
- use bytes;
- $charpos = length $self->{eaten};
- }
- return ' in '.$self->{options}->{input_filename}.', line '.($self->{lineno}+1).
- ', character '.$charpos.', marked by ^^^ in '.$line;
-}
-
-sub warn
-{
- my $self = shift;
- my ($text) = @_;
- $self->{options}->error($text.$self->errorinfo());
-}
-
-package VMXTemplate::Utils;
-
-use base qw(Exporter);
-our @EXPORT = qw(
- TS_UNIX TS_DB TS_DB_DATE TS_MW TS_EXIF TS_ORACLE TS_ISO_8601 TS_RFC822
- timestamp plural_ru strlimit strip_tags addcslashes
-);
-
-use constant {
- TS_UNIX => 0,
- TS_DB => 1,
- TS_DB_DATE => 2,
- TS_MW => 3,
- TS_EXIF => 4,
- TS_ORACLE => 5,
- TS_ISO_8601 => 6,
- TS_RFC822 => 7,
-};
-
-my @Mon = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
-my %mon = qw(jan 0 feb 1 mar 2 apr 3 may 4 jun 5 jul 6 aug 7 sep 8 oct 9 nov 10 dec 11);
-my @Wday = qw(Sun Mon Tue Wed Thu Fri Sat);
-
-our $safe_tags = 'div|blockquote|span|a|b|i|u|p|h1|h2|h3|h4|h5|h6|strike|strong|small|big|blink|center|ol|pre|sub|sup|font|br|table|tr|td|th|tbody|tfoot|thead|tt|ul|li|em|img|marquee|cite';
-
-# ограниченная распознавалка дат
-sub timestamp
-{
- my ($ts, $format) = @_;
-
- require POSIX;
- if (int($ts) eq $ts)
- {
- # TS_UNIX or Epoch
- $ts = time if !$ts;
- }
-
- elsif ($ts =~ /^\D*(\d{4,}?)\D*(\d{2})\D*(\d{2})\D*(?:(\d{2})\D*(\d{2})\D*(\d{2})\D*([\+\- ]\d{2}\D*)?)?$/so)
- {
- # TS_DB, TS_DB_DATE, TS_MW, TS_EXIF, TS_ISO_8601
- $ts = POSIX::mktime($6||0, $5||0, $4||0, $3, $2-1, $1-1900);
- }
- elsif ($ts =~ /^\s*(\d\d?)-(...)-(\d\d(?:\d\d)?)\s*(\d\d)\.(\d\d)\.(\d\d)/so)
- {
- # TS_ORACLE
- $ts = POSIX::mktime($6, $5, $4, int($1), $mon{lc $2}, $3 < 100 ? $3 : $3-1900);
- }
- elsif ($ts =~ /^\s*..., (\d\d?) (...) (\d{4,}) (\d\d):(\d\d):(\d\d)\s*([\+\- ]\d\d)\s*$/so)
- {
- # TS_RFC822
- $ts = POSIX::mktime($6, $5, $4, int($1), $mon{lc $2}, $3-1900);
- }
- else
- {
- # Bogus value, return undef
- return undef;
- }
-
- if (!$format)
- {
- # TS_UNIX
- return $ts;
- }
- elsif ($format == TS_MW)
- {
- return POSIX::strftime("%Y%m%d%H%M%S", localtime($ts));
- }
- elsif ($format == TS_DB)
- {
- return POSIX::strftime("%Y-%m-%d %H:%M:%S", localtime($ts));
- }
- elsif ($format == TS_DB_DATE)
- {
- return POSIX::strftime("%Y-%m-%d", localtime($ts));
- }
- elsif ($format == TS_ISO_8601)
- {
- return POSIX::strftime("%Y-%m-%dT%H:%M:%SZ", localtime($ts));
- }
- elsif ($format == TS_EXIF)
- {
- return POSIX::strftime("%Y:%m:%d %H:%M:%S", localtime($ts));
- }
- elsif ($format == TS_RFC822)
- {
- my @l = localtime($ts);
- return POSIX::strftime($Wday[$l[6]].", %d ".$Mon[$l[4]]." %Y %H:%M:%S %z", @l);
- }
- elsif ($format == TS_ORACLE)
- {
- my @l = localtime($ts);
- return POSIX::strftime("%d-".$Mon[$l[4]]."-%Y %H.%M.%S %p", @l);
- }
- return $ts;
-}
-
-# Select one of 3 plural forms for russian language
-sub plural_ru
-{
- my ($count, $one, $few, $many) = @_;
- my $sto = $count % 100;
- if ($sto >= 10 && $sto <= 20)
- {
- return $many;
- }
- my $r = $count % 10;
- if ($r == 1)
- {
- return $one;
- }
- elsif ($r >= 2 && $r <= 4)
- {
- return $few;
- }
- return $many;
-}
-
-# Limit string to $maxlen
-sub strlimit
-{
- my ($str, $maxlen, $dots) = @_;
- if (!$maxlen || $maxlen < 1 || length($str) <= $maxlen)
- {
- return $str;
- }
- $str = substr($str, 0, $maxlen);
- my $p = rindex($str, ' ');
- if ($p < 0 || (my $pt = rindex($str, "\t")) > $p)
- {
- $p = $pt;
- }
- if ($p > 0)
- {
- $str = substr($str, 0, $p);
- }
- return $str . (defined $dots ? $dots : '...');
-}
-
-# Replace (some) tags with whitespace
-sub strip_tags
-{
- my ($str, $allowed) = @_;
- my $allowed = $allowed ? '(?!/?('.$allowed.'))' : '';
- $str =~ s/(<$allowed\/?[a-z][a-z0-9-]*(\s+[^<>]*)?>\s*)+/ /gis;
- return $str;
-}
-
-# Add '\' before specified chars
-sub addcslashes
-{
- my ($str, $escape) = @_;
- $str =~ s/([$escape])/\\$1/gs;
- return $str;
-}