#!/usr/bin/perl
# $Id: plSmarty.pm,v 1.1 2005/11/28 21:53:35 sharky Exp $
#
# plSmarty is a light-weight template engine that uses a
# subset of Smarty's tags.
#
# Copyright (c) 2003 Jeremy Lainé

#
# The supported tags are:
#
# 1. Variables access
# -------------------
#
#  Scalar : {$myvar}
#
#  Hash   : {$myvar.EXPR}
#
#  Array  : {$myvar[EXPR]}
#
#
# 2. Conditionals
# ---------------
#  {if EXPR}
#  ..
#  {else}
#  ..
#  {/if}
#
#
# 3. Iteration
# ------------
#
#  {foreach from=$myvar item="myitem"}
#  The item : {$myitem}
#  {/foreach}
#

package plSmarty;

use strict;
use CGI qw(:standard);

#
#  PRIVATE FUNCTIONS
#


# Evaluate an expression from a template
#
# TODO make this function private
sub evaluate {
  my $self = shift;
  my $expr = shift;

  my %vars = %{$self->{myvars}};

  if ($expr =~ /^((['"])([^\2]+)\2|\w+)$/) {
    # this is a literal
    return $3 ? $3 : $1;

  } elsif ($expr =~ /^\$(\w+)$/) {
    # this is a scalar variable
    return $vars{$1};

  } elsif ($expr =~ /^\$(\w+)\[([^\]]+)\]$/) {
    # this is an element of an array
    my $arr = $1;
    my $key = $self->evaluate($2);

    if (defined($vars{$arr})) {
      my @varr = @{$vars{$arr}};
      return $varr[$key];
    } else {
      return;
    }

  } elsif ($expr =~ /^\$([a-zA-Z]+)\.(.+)$/) {
    # this is an element of a hash
    my $arr = $1;
    my $key = $self->evaluate($2);

    if (defined($vars{$arr})) {
      my %vhash = @{$vars{$arr}};
      return $vhash{$key};
    } else {
      return;
    }
  } else {
    # parse error
    die("could not evaluate `$expr`");
  }

};


# Try to locate a template
my $find = sub {
  my $self = shift;
  my $file = shift;

  my @dirs = @{$self->{myconf}{template_path}};
  foreach my $dir(@dirs) {
    if ( -e "$dir/$file" ) {
      return "$dir/$file";
    }
  }

  die("Could not find template '$file'");
};



# Parse an array of tokens
#
# TODO support custom functions
my $parse;
$parse = sub {
  my ($self,@tokens) = @_;

  my %vars = %{$self->{myvars}};
  my @output;

  #foreach my $token(@tokens) {
  while (defined(my $token = shift @tokens)) {
    # replace tags
    if ($token =~ /^{\*(.*)\*}$/) {
      # this is a comment, skip it
    } elsif ($token =~ /^{(\$.+)}$/) {
      # replace variable by its value
      push @output,$self->evaluate($1);

    } elsif ($token =~ /^{include file='([^']+)'}$/) {
      # include another template
      push @output, $self->fetch($1);

    } elsif ($token =~ /^{if ([^}]+)}$/) {
      # conditional expression
      my $expr = $1;
      my $true = $self->evaluate($expr);
      #die("$expr : $true");
      #my $true = 0;
      #if ($expr =~ /^\$([a-zA-Z]+)$/) {
      #  $true = $vars{$1};
      #}

      my @block;
      my $level = 1;

      while ($level && defined($token = shift @tokens)) {
        if ($token =~ /^{if ([^}]+)}$/) {
          $level++;

        } elsif ($token =~ /^{\/if}$/) {
          # end of if statement

          $level --;
          if ($level == 0) {
            if ($true) {
              push @output,$self->$parse(@block);
            }
          } else {
            push @block,$token;
          }

        } elsif (($level == 1) && ($token =~ /^{else}$/)) {
          # else statement at our level

          if ($true) {
            push @output,$self->$parse(@block);
          }
          @block = ();
          $true = !$true;

        } else {
          # add line to current conditional block

          push @block,$token;
        }
      }

      # check we found end tag
      if ($level) {
        die("parse error, could not find matching {/if}!")
      }


    } elsif ($token =~ /^{foreach from=\$([a-zA-Z]+) item=([a-zA-Z]+)}$/) {

      # foreach loop statement
      my ($from,$item) = ($1,$2);
      my @block;
      my $level = 1;

      while ($level && defined($token = shift @tokens)) {
        if ($token =~ /^{foreach from=\$([a-zA-Z]+) item=([a-zA-Z]+)}$/) {
          # nested foreach loop

          $level++;

        } elsif ($token =~ /^{\/foreach}$/) {
          # end of a foreach loop

          $level --;
          if ($level == 0) {
            # iterate over $from
            foreach my $myval(@{$vars{$from}}) {
              $self->assign($item,$myval);
              push @output,$self->$parse(@block);
            }

          } else {
            push @block,$token;
          }

        } else {
          # add line to current loop block

          push @block,$token;
        }
      }

      # check we found end tag
      if ($level) {
        die("parse error, could not find matching {/foreach}!")
      }

    } elsif ($token =~ /^{\w+ \w=(\$\w+)}$/) {

      # custom function

    } else {
      push @output,$token;
    }

  }

  return @output;
};



#
#  PUBLIC FUNCTIONS
#


# The constructor
sub new {
  my $invocant = shift;
  my $class = ref($invocant) || $invocant;
  my $self = {
    myvars => {},
    mytags => {},
    myconf => {}
  };
  bless ($self, $class);

  # retrieve POST and GET variables
  my %request;
  foreach my $key(param) {
    $request{$key} = param($key);
  }
  $self->assign('request',%request);
  $self->config('template_path',("."));
  $self->assign('poweredby',"Powered by <a href=\"http://dev.jerryweb.org/plSmarty/\">plSmarty</a>");

  # other values
  $self->init();
  return $self;
}


# Extra initialisation
sub init {

}

# Append to an array variable
sub append {
  my $self = shift;
  my $var = shift;
  my %hash = %$self;

  push @{$hash{myvars}{$var}}, (@_>1) ? [ @_ ] : shift;
}


# Performs a variable assignement for substitution in templates.
sub assign {
  my $self = shift;
  my $var = shift;
  my %hash = %$self;

  if (@_ > 1) {
    @{$hash{myvars}{$var}} = ( @_ );
  } else {
    $hash{myvars}{$var} = shift;
  }

}


#  This displays the template
sub display {
  my $self = shift;
  my $template = shift;
  print $self->fetch($template);
}


# This returns the template output instead of displaying it.
sub fetch {
  my $self = shift;
  my $template = shift;
  $template = $self->$find($template);

  # read template
  open(TEMPLATE,"<$template");
  my @lines = <TEMPLATE>;
  close(TEMPLATE);

  # break into tokens and parse them
  my @tokens = split(/({[^{]*})/,(join "",@lines));
  return $self->$parse(@tokens);
}


# Register a custom function
sub register_function {
  my $self = shift;
  my $tag = shift;
  my $func = shift;

  die("Could not find function `$func`!")
    if (!exists(&$func));

  my %hash = %$self;
  $hash{mytags}{$tag} = $func;
}


# Set a configuration variable
sub config {
  my $self = shift;
  my $var = shift;
  my %hash = %$self;

  @{$hash{myconf}{$var}} = ( @_ );
}

1;
