# A really simple template engine
-# (c) 2004--2008 Martin Mares <mj@ucw.cz>
+# (c) 2004--2012 Martin Mares <mj@ucw.cz>
package UCW::Temple;
$deps{$_[0]} = 1;
}
+my $current_file_name;
+my $current_string;
+my $current_pos;
+
+sub error($;$) {
+ my ($msg, $offset) = @_;
+ if (defined $current_file_name) {
+ #
+ # This is rather tricky. We want to report the exact place, where the error
+ # occurred, but the cost of keeping track of the current line number is too high.
+ # Hence we keep just the current position inside the input stream and convert
+ # it to the line number only when reporting an error.
+ #
+ my $input = substr(${$current_string}, 0, $current_pos);
+ my @newlines = ($input =~ m{\n}g);
+ my $line = @newlines + ($offset // 1);
+ die "$current_file_name:$line: $msg\n";
+ } else {
+ die "$msg\n";
+ }
+}
+
sub get_nested($$$) {
my ($r,$left,$right) = @_;
pos $$r = pos($$r)-1;
} elsif ($$r =~ /\G([$right])/cgs) {
$nest--;
} else {
- die "File ended when looking for matching $right";
+ error "File ended when looking for matching $right";
}
$z .= $1;
} while ($nest);
my $x = shift;
my $res = eval "package T; $x";
return $res unless $@;
- die "Error evaluating $x: $@";
+ my $m = $@;
+ chomp $m;
+ if (my ($msg, $line) = $m =~ m{(.*) at \(eval \d+\) line (\d+)\.$}) {
+ error $msg, $line;
+ } else {
+ error $m;
+ }
} else {
return "";
}
my ($f, $a) = @_;
if ($f =~ /^(if|fi|else|elif)$/) {
if ($f eq "if") {
- $a ne "()" or die "\@if requires an argument";
+ $a ne "()" or error "\@if requires an argument";
if ($cond[0] > 0) {
unshift @cond, (eval_if_ok($a) ? 1 : -1);
} else {
unshift @cond, 0;
}
} elsif ($f eq "fi") {
- $a eq "()" or die "\@fi takes no arguments";
- $#cond or die "\@fi without \@if";
+ $a eq "()" or error "\@fi takes no arguments";
+ $#cond or error "\@fi without \@if";
shift @cond;
} elsif ($f eq "else") {
- $a eq "()" or die "\@else takes no arguments";
- $#cond or die "\@else without \@if";
+ $a eq "()" or error "\@else takes no arguments";
+ $#cond or error "\@else without \@if";
$cond[0] = -$cond[0];
} elsif ($f eq "elif") {
- $a ne "()" or die "\@elif requires an argument";
- $#cond or die "\@elif without \@if";
+ $a ne "()" or error "\@elif requires an argument";
+ $#cond or error "\@elif without \@if";
if ($cond[0] > 0) {
$cond[0] = 0;
} elsif ($cond[0] < 0) {
}
}
-sub parse_string($) {
- my ($t) = @_;
+sub parse_string($$) {
+ my ($t, $name) = @_;
+ my ($old_fn, $old_str, $old_pos) = ($current_file_name, $current_string, $current_pos);
+ $current_file_name = $name;
+ $current_string = \$t;
pos $t = 0;
for(;;) {
+ $current_pos = pos $t;
if ($t =~ /\G([^\@]+)/cgs) {
out $1 if $cond[0] > 0;
} elsif ($t =~ /\G\@\s*\n/cgs) {
} elsif ($t =~ /\G\@(\$\w+)/cgs) {
out eval_if_ok($1);
} elsif ($t =~ /\G(\@[^\n]*)/cgs) {
- die "Unknown control sequence $1";
+ error "Unknown control sequence $1";
} elsif ($t =~ /\G$/cgs) {
last;
} elsif ($t =~ /\G([^\n]*)/cgs) {
- die "Internal parser error at $1 (pos " . pos($t) . ")";
+ error "Internal parser error";
} else { die; }
}
+ ($current_file_name, $current_string, $current_pos) = ($old_fn, $old_str, $old_pos);
}
sub parse_file($) {
my ($name) = @_;
add_depend($name);
my $fh = new IO::File $name;
- die "Unable to open $name: $!" unless defined $fh;
+ error "Unable to open $name: $!" unless defined $fh;
my $text;
{ local $/; undef $/; $text = <$fh>; }
undef $fh;
- parse_string($text);
+ parse_string($text, $name);
}
sub start(;$)
sub finish()
{
- $#cond and die "Unterminated \@if (depth $#cond)";
+ $#cond and error "Unterminated \@if (depth $#cond)";
}
sub process_file($;$) {
sub process_string($;$) {
my ($string, $args) = @_;
start($args);
- parse_string($string);
+ parse_string($string, '<string>');
finish();
}
import UCW::Temple;
+# Beware: Functions in the T package should report errors via die(), not error().
+
our $temp;
our $out_func = sub { print @_; };