#!/usr/bin/perl
# A really simple template engine
# (c) 2004 Martin Mares <mj@ucw.cz>

package UCW::Temple;

use strict;
use warnings;
no strict 'vars';

use IO::File;

our @cond;
our $temp;

sub get_nested($$$) {
	my ($r,$left,$right) = @_;
	pos $$r = pos($$r)-1;
	my $z = "";
	my $nest = 0;
	do {
		if ($$r =~ /\G([^$left$right]+)/cgs) {
		} elsif ($$r =~ /\G([$left])/cgs) {
			$nest++;
		} elsif ($$r =~ /\G([$right])/cgs) {
			$nest--;
		} else {
			die "File ended when looking for matching $right";
		}
		$z .= $1;
	} while ($nest);
	return $z;
}

sub eval_if_ok($) {
	if ($cond[0] > 0) {
		my $x = shift;
		my $res = eval $x;
		return $res unless $@;
		die "Error evaluating $x: $@";
	} else {
		return "";
	}
}

sub eval_func($$) {
	my ($f, $a) = @_;
	if ($f =~ /^(if|fi|else|elif)$/) {
		if ($f eq "if") {
			$a ne "()" or die "\@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";
			shift @cond;
		} elsif ($f eq "else") {
			$a eq "()" or die "\@else takes no arguments";
			$#cond or die "\@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";
			if ($cond[0] > 0) {
				$cond[0] = 0;
			} elsif ($cond[0] < 0) {
				$cond[0] = 1;
				$cond[0] = (eval_if_ok($a) ? 1 : -1);
			}
		}
		# print "Cond stack: @cond\n";
	} else {
		eval_if_ok("$f($a)");
	}
}

sub parse_string($) {
	my ($t) = @_;
	pos $t = 0;
	for(;;) {
		if ($t =~ /\G([^\@]+)/cgs) {
			print $1 if $cond[0] > 0;
		} elsif ($t =~ /\G\@\s*\n/cgs) {
			# @ at end of line is ignored and eats the end of line
		} elsif ($t =~ /\G\@#[^\n]*\n/cgs) {
			# a comment, which is ignored
		} elsif ($t =~ /\G\@\@/cgs) {
			print "\@";
		} elsif ($t =~ /\G\@{/cgs) {
			my $x = get_nested(\$t, "{", "}");
			print eval_if_ok($x);
		} elsif ($t =~ /\G\@\[/cgs) {
			my $x = get_nested(\$t, '\[', '\]');
			$x =~ s/^\[//;
			$x =~ s/\$\]//;
			eval_if_ok($x);
		} elsif ($t =~ /\G\@(\w+)\(/cgs) {
			my $func = $1;
			my $args = get_nested(\$t, '(', ')');
			eval_func($func, $args);
		} elsif ($t =~ /\G\@(\w+)([^\n]*)\n/cgs) {
			eval_func($1, "($2)");
		} elsif ($t =~ /\G\@(\$\w+)/cgs) {
			print eval_if_ok($1);
		} elsif ($t =~ /\G(\@[^\n]*)/cgs) {
			die "Unknown control sequence $1";
		} elsif ($t =~ /\G$/cgs) {
			last;
		} elsif ($t =~ /\G([^\n]*)/cgs) {
			die "Internal parser error at $1 (pos " . pos($t) . ")";
		} else { die; }
	}
}

sub parse_file($) {
	my ($name) = @_;
	my $fh = new IO::File $name;
	die "Unable to open $name: $!" unless defined $fh;
	my $text;
	{ local $/; undef $/; $text = <$fh>; }
	undef $fh;
	parse_string($text);
}

sub start()
{
	@cond = (1);
}

sub finish()
{
	$#cond and die "Unterminated \@if (depth $#cond)";
}

sub process_file($) {
	start();
	parse_file($_[0]);
	finish();
}

sub process_string($) {
	start();
	parse_string($_[0]);
	finish();
}

### Commands available from the templates

sub include {
	my $fn = shift @_;
	while (@_) {
		my $v = shift @_;
		$temp = shift @_;
		eval_if_ok("\$$v = \$temp");
	}
	parse_file($fn);
}

sub load {
	my $f = shift @_;
	(!defined $f || @_) and die "\@load requires only one argument";
	require $f;
}

1;
