1 # A really simple template engine
2 # (c) 2004--2015 Martin Mares <mj@ucw.cz>
13 our @ISA = qw(Exporter);
14 our @EXPORT = qw(out);
15 our @EXPORT_OK = qw(set);
40 my $current_file_name;
46 my ($msg, $offset) = @_;
47 if (defined $current_file_name && !$reported_error) {
49 # This is rather tricky. We want to report the exact place, where the error
50 # occurred, but the cost of keeping track of the current line number is too high.
51 # Hence we keep just the current position inside the input stream and convert
52 # it to the line number only when reporting an error.
54 my $input = substr(${$current_string}, 0, $current_pos);
55 my @newlines = ($input =~ m{\n}g);
56 my $line = @newlines + ($offset // 1);
58 die "$current_file_name:$line: $msg\n";
65 my ($r, $left, $right) = @_;
66 pos $$r = pos($$r) - 1;
70 if ($$r =~ /\G([^$left$right]+)/cgs) {
71 } elsif ($$r =~ /\G([$left])/cgs) {
73 } elsif ($$r =~ /\G([$right])/cgs) {
76 error "File ended when looking for matching $right";
83 # In addition to get_nested(), skip single-quoted and double-quoted string literals
84 sub get_function_args($$$) {
85 my ($r, $left, $right) = @_;
86 pos $$r = pos($$r) - 1;
90 if ($$r =~ /\G([^$left$right"']+)/cgs
91 || $$r =~/\G("(\\.|[^"\\])*")/cgs # double-quoted string
92 || $$r =~/\G('(\\.|[^'\\])*')/cgs # single-quoted string
94 } elsif ($$r =~ /\G([$left])/cgs) {
96 } elsif ($$r =~ /\G([$right])/cgs) {
99 error "File ended when looking for matching $right";
109 my $res = eval "package T; $x";
110 return $res unless $@;
113 if (my ($msg, $line) = $m =~ m{(.*) at \(eval \d+\) line (\d+)\.$}) {
125 if ($f =~ /^(if|fi|else|elif)$/) {
127 $a ne "()" or error "\@if requires an argument";
129 unshift @cond, (eval_if_ok($a) ? 1 : -1);
133 } elsif ($f eq "fi") {
134 $a eq "()" or error "\@fi takes no arguments";
135 $#cond or error "\@fi without \@if";
137 } elsif ($f eq "else") {
138 $a eq "()" or error "\@else takes no arguments";
139 $#cond or error "\@else without \@if";
140 $cond[0] = -$cond[0];
141 } elsif ($f eq "elif") {
142 $a ne "()" or error "\@elif requires an argument";
143 $#cond or error "\@elif without \@if";
146 } elsif ($cond[0] < 0) {
148 $cond[0] = (eval_if_ok($a) ? 1 : -1);
151 # print "Cond stack: @cond\n";
153 my $res = eval_if_ok("$f $a");
154 out $res if defined $res;
158 sub parse_string($$) {
160 my ($old_fn, $old_str, $old_pos) = ($current_file_name, $current_string, $current_pos);
161 $current_file_name = $name;
162 $current_string = \$t;
165 $current_pos = pos $t;
166 # Scan for the first occurrence of an active character ("@", "~")
167 if ($t =~ /\G([^\@~]+)/cgs) {
168 out $1 if $cond[0] > 0;
169 } elsif ($t =~ /\G~/cgs) {
170 out $T::tilde if $cond[0] > 0;
171 } elsif ($t =~ /\G\@~/cgs) {
172 out "~" if $cond[0] > 0;
173 } elsif ($t =~ /\G\@\s*\n/cgs) {
174 # @ at end of line is ignored and eats the end of line
175 } elsif ($t =~ /\G\@#[^\n]*\n/cgs) {
176 # a comment, which is ignored
177 } elsif ($t =~ /\G\@\@/cgs) {
178 out "\@" if $cond[0] > 0;
179 } elsif ($t =~ /\G\@\{/cgs) {
180 my $x = get_nested(\$t, '\{', '\}');
182 } elsif ($t =~ /\G\@\[/cgs) {
183 my $x = get_nested(\$t, '\[', '\]');
187 } elsif ($t =~ /\G\@\(/cgs) {
188 my $x = get_nested(\$t, '\(', '\)');
191 out (defined($arguments->{$x}) ? $arguments->{$x} : "") if $cond[0] > 0;
192 } elsif ($t =~ /\G\@(\w+)\(/cgs) {
194 my $args = get_function_args(\$t, '(', ')');
195 eval_func($func, $args);
196 } elsif ($t =~ /\G\@(\w+)([^\n]*)\n/cgs) {
197 eval_func($1, "($2)");
198 } elsif ($t =~ /\G\@(\$\w+)/cgs) {
200 } elsif ($t =~ /\G(\@[^\n]*)/cgs) {
201 error "Unknown control sequence $1";
202 } elsif ($t =~ /\G$/cgs) {
204 } elsif ($t =~ /\G([^\n]*)/cgs) {
205 error "Internal parser error";
208 ($current_file_name, $current_string, $current_pos) = ($old_fn, $old_str, $old_pos);
214 open my $fh, '<:utf8', $name or error "Unable to open $name: $!";
216 { local $/; undef $/; $text = <$fh>; }
218 parse_string($text, $name);
228 $#cond and error "Unterminated \@if (depth $#cond)";
231 sub process_file($;$) {
232 my ($name, $args) = @_;
238 sub process_string($;$) {
239 my ($string, $args) = @_;
241 parse_string($string, '<string>');
245 ### Perl commands embedded in the templates are evaluated in this package ###
251 # Beware: Functions in the T package should report errors via die(), not error().
254 our $out_func = sub { print @_; };
262 UCW::Temple::eval_if_ok("\$$v = \$temp");
264 UCW::Temple::parse_file($fn);
270 (!defined($f) || @_) and die "\@load requires only one argument";
271 UCW::Temple::add_depend($f);
277 return $UCW::Temple::arguments->{$_[0]};