The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Class::Skin;

use 5.006;
use strict;
use warnings;
use Carp;

require Exporter;
require DynaLoader;
use AutoLoader;

our @ISA = qw(Exporter DynaLoader);

our $VERSION = '0.05';

use Carp;
use strict;
use Log::NullLogLite;
use bytes;
use Cwd;

sub AUTOLOAD {
    # This AUTOLOAD is used to 'autoload' constants from the constant()
    # XS function.  If a constant is not found then control is passed
    # to the AUTOLOAD in AutoLoader.

    my $constname;
    our $AUTOLOAD;
    ($constname = $AUTOLOAD) =~ s/.*:://;
    croak "& not defined" if $constname eq 'constant';
    my $val = constant($constname, @_ ? $_[0] : 0);
    if ($! != 0) {
	if ($! =~ /Invalid/ || $!{EINVAL}) {
	    $AutoLoader::AUTOLOAD = $AUTOLOAD;
	    goto &AutoLoader::AUTOLOAD;
	}
	else {
	    croak "Your vendor has not defined Class::Skin macro $constname";
	}
    }
    {
	no strict 'refs';
	# Fixed between 5.005_53 and 5.005_61
	if ($] >= 5.00561) {
	    *$AUTOLOAD = sub () { $val };
	}
	else {
	    *$AUTOLOAD = sub { $val };
	}
    }
    goto &$AUTOLOAD;
}

bootstrap Class::Skin $VERSION;

# Preloaded methods go here.

#############################################################
# new()
# new($file_path) 
# new($file_path, $log) 
#############################################################
# the constructor
sub new {
    my $proto = shift; # get the class name
    my $class = ref($proto) || $proto;
    my $self  = {};
    # private data
    $self->{FILE_PATH} = shift;
    $self->{LOG} = shift || new Log::NullLogLite();
    $self->{LINES} = undef;
    $self->{SKIP_INCLUDES} = 0; # by deafult we process the includes
    $self->{CLEAN_EMPTY_LINES} = 1; # by deafult we clean empty lines
    $self->{CWD} = getcwd();
    # by default, contains the current directory
    $self->{DIRECTORY_LIST} = $self->{CWD};
    bless ($self, $class);
    return $self;
} # of new

####################################
# log()
####################################
sub log {
    my $self = shift;
    if (@_) { $self->{LOG} = shift }
    return $self->{LOG};
} # of log

####################################
# skip_includes()
####################################
sub skip_includes {
    my $self = shift;
    if (@_) { $self->{SKIP_INCLUDES} = shift }
    return $self->{SKIP_INCLUDES};
} # of skip_includes

####################################
# clean_empty_lines()
####################################
sub clean_empty_lines {
    my $self = shift;
    if (@_) { $self->{CLEAN_EMPTY_LINES} = shift }
    return $self->{CLEAN_EMPTY_LINES};
} # of clean_empty_lines

####################################
# file_path()
####################################
sub file_path {
    my $self = shift;
    if (@_) { $self->{FILE_PATH} = shift }
    return $self->{FILE_PATH};
} # of file_path

####################################
# directory_list()
####################################
sub directory_list {
    my $self = shift;
    if (@_) { $self->{DIRECTORY_LIST} = shift }
    return $self->{DIRECTORY_LIST};
} # of directory_list

####################################
# lines()
####################################
sub lines {
    my $self = shift;
    if (@_) { $self->{LINES} = shift }
    return $self->{LINES};
} # of lines

####################################
# read()
####################################
sub read {
    my $self = shift;
    my $path = shift || $self->{FILE_PATH};    
    my $path_dir = $path;
    if ($path =~ /\/[^\/]+$/) {
	$path_dir = $`;
    }
    $self->{DIRECTORY_LIST} .= ",$path_dir";

    # we use the other target lines reference in order to read the template
    # into other target then the lines of the object. this way we can call
    # the read method in order to read included templates.
    my $other_target_lines_ref = shift || 0;
    # check to see if $self->{FILE_PATH} is valid
    unless (-e $path) {
	# first check if it is relative or absolute path:
	unless (is_absolute_path($path)) { # relative path
	    foreach my $directory (split(/,/, $self->{DIRECTORY_LIST})) {
		if (-e $directory."/".$path) {
		    $path = $directory."/".$path;
		    last;
		}
	    }	    
	}
    }
    # open the file
    open(FILE, $path) || do {
	$self->{LOG}->write("Could not open to read the file ".
			    $path.": ".$! , 4);
	$self->{LOG}->write("the directories in the directory list are: ".
			    $self->{DIRECTORY_LIST}, 4);
	return 0;
    };

    # read the lines from the file
    if ($other_target_lines_ref) {
	read(FILE, $$other_target_lines_ref, -s FILE);
    }
    else {
	read(FILE, $self->{LINES}, -s FILE);
    }
    return 1; # success
} # of read

####################################
# $html = parse({ var1 => val1,
#                 var2 => val2,
#                 ...
#               })
####################################
sub parse {
    my $self = shift;
    my $variables = shift;
    my $lines = $self->{LINES};
    return xs_parse($self, $variables, $lines);    
}

################
# perl_write_log
################
sub perl_write_log {
    my $self = shift;
    my $message = shift;
    my $level = shift;
    $self->{LOG}->write($message, $level);
} # of perl_write_log

######################
# is_absolute_path
######################
sub is_absolute_path {
    my $path = shift;

    # Returns 1 if path supplied is an absolute path
    # Returns 0 if path is not absolute.
    unless (defined($path)) {
	return 0;
    }
    # the different Operating Systems
    my %operating_systems = ( "mswin32"  => '^(?:[a-zA-Z]:)?[\\\/]+',
			      "linux"    => '^\/',
			      "cygwin"   => '^([A-Za-z]:)|^(\/)');
    
    my $os = lc($^O);
    my $reg_expression = $operating_systems{$os} || 
	$operating_systems{'linux'};    
    return $path =~ /$reg_expression/;
} # is_absolute_path


# Autoload methods go after =cut, and are processed by the autosplit program.

1;
__END__
# Below is stub documentation for your module. You better edit it!

=head1 NAME

Class::Skin - Class for creating text out of text templates.

=head1 SYNOPSIS

  use Class::Skin;
  
  # create new Class::Skin object
  my $skin = new Class::Skin("template.txt");

  # read the template file 
  $skin->read(); 

  # parse the template and print the result. we send in anonymous 
  # hash all the variables that we would like to use in the template
  print $skin->parse({ title => $title,
		       i => $i,
		       condition1 => 1,
		       condition2 => 0,
		       condition3 => 1,
		       condition4 => 0,
		       condition5 => 1,
		       condition6 => 0,
		       condition7 => 1,
		       condition8 => 0,
		       condition9 => 1,
		       filepath   => "template2.txt",
		       func       => \&call_back_func});

=head1 DESCRIPTION

The elements that can be used in the templates:

=over 4

=item Comments 

Comments will just be removed from the template when the template is parsed.
                  <comment>
                    this is a comment
                  </comment> 


=item Perl Variables 

Variables will be sent to the Class::Skin object when parsing as reference 
to anonymous hash.
                  $name 

=item Including other files 

Includes will be parsed and included instead of the include tag.  
There is a way to avoid from including all the includes while parsing the 
template (see skip_includes method). This might be useful when parsing
the template will create yet another template that will be parsed again 
later. There is also a way to skip the including for certain include, for
certain times that that include is parsed (so that the parse method is called).
That is done by using the attribute skip. Every time the method parse is called
the value of the skip attribute will be decremented. If it is zero, the parse
will include the template that is defined in the src.

                  <include src="other_template.txt">
                  <include src="$name"> 
                  <include src="template.txt" skip="2">

The included template path can be absolute, or relative. If it is relative, 
it will be searched, relative to the host template path, or relative to the 
list of directories. This list is populated by default by the path of the 
current directory and the path of the hosted templates. It can be manipulated 
by the access method I<directory_list>.

=item If-elsif-else 

The condition blocks are solved using the values of the variables that are 
put as the conditions. Note that it is possible to have one block inside other.
                  <if condition="name1">
                    <h1> positive... </h1>
                  </if>
                  <elsif condition="name2">
                    <h1> Negative... </h1>
                  </elsif>
                  <else>
                    <h1> Zero... </h1>
                  </else> 
Any white characters betweeb a closing tag of if-elsif-else block and the 
next opening tag of yet the same block will be ignored.  

=item While loop 

While blocks will be executed as long as the callback function returns true. 
The callback function reference is sent as variable in the anonymous hash. 
The callback function must take as its only argument the same anonymous 
hash as the anonymous hash that is sent to the parse method. See the callback
function in the example.
                  <while condition="function_name">
                    <tr>
                      <td>$i</td>
                    </tr>
                  </while> 

=back

=head1 CONSTRUCTOR

=over 4

=item new ( [FILEPATH [, LOG ]] )

The constructor. If FILEPATH is defined, it will be taken as the file path 
of the template. If LOG is defined, it will be used as a Log object for 
logging errors. It is recommended to use the LOG as it helps finding errors
in using the class (like trying to include a file that is not found or
trying to use a while loop with call back function that does not exist).
Returns the new object. 

=back

=head1 METHODS

=over 4

=item read()

This method tries to open the template file (using the FILE_PATH data member)
and to read its content to the LINES data member.
Returns 1 on success, 0 on failure. 

=item parse( [ REFERENCE_TO_ANONYMOUS_HASH ] )

This method will parse the template using the anonymous hash variable that 
is sent to it as a parameter.
For details about the parsing rules see above.
Returns the parsed template on success or undef on failure. 

=item lines( [ LINES ] )

The access method to the LINES data member. This data member is a string 
that holds all the lines of a read template. If LINES is defined, its value 
will replace the current value of the LINES data member (as if we read a 
new template).
Returns the value of the LINES data member. 

=item log( [ LOG ] )

The access method to the LOG data member. If the LOG data member is defined, 
error messages will be logged using that Log object.
Returns the value of the LOG data member. 

=item skip_includes( [ BOOLEAN ] )

This access method control if we should skip includes while parsing the 
template. This can be useful when trying to create template of template - i.e.
when we create a template that when parsed will create other template. 
In this case we can put tags that we do not want to parse in the first pass
in the included templates, and of course, we should skip the includes by 
sending TRUE to this method.
Returns the value of the SKIP_INCLUDES data member.

=item clean_empty_lines( [ BOOLEAN ] ) B<NOT YET IMPLEMENTED>

This access method control if after parsing, empty lines will be cleaned.
This might be useful when creating HTMLs. Empty lines in HTMLs are not 
visible anyway, and can be cleaned. By sending TRUE to this method, we 
can set the object to clean empty lines.
Returns the value of the CLEAN_EMPTY_LINES data member.

=item file_path( [ FILEPATH ] )

The access method to the FILE_PATH data member. By using this method we
can change the path of the template that we are going to read.
Returns the value of the FILE_PATH data member.

=item directory_list( [ DIRECTORY_LIST ] )

The access method to the DIRECTORY_LIST data member. It holds a comma 
separated list of directories. When using the include tag, and the path of 
the included template is relative, it will be taken first relative to the 
path of the hosted template. If the included template will not be found 
there, it will be checked relative to the directories in the DIRECTORY_LIST.
The DIRECTORY_LIST is populated by default with the current working directory.
Returns the value of the DIRECTORY_LIST.

=back

=head1 EXAMPLE

Here is template1.txt:

 <comment> this comment will not be shown in the final html </comment>
 <comment> the title will be in "title"</comment>
 *** $title ***

 Here two templates will be included:
 <comment> including template with its filepath </comment>
 <include src="template2.txt">

 <comment> 
  including template that its path is in a variable called filepath 
 </comment>
 <include src="$filepath">

 Here we will put some conditions:
 <if condition="condition1">
  condition1 is true
  <if condition="condition2">
   condition2 is true (in condition1)
  </if>
  <if condition="condition3">
   condition3 is true (in condition1)
   <if condition="condition5">
    condition5 is true (in condition3 that in condition1)
   </if>
   <else>
    condition5 is false (in condition3 that in condition1)
   </else>
  </if>
 </if>
 <elsif condition="condition2">
  condition2 is true
 </elsif>
 <if condition="condition4">
  condition4 is true
 </if>
 <elsif condition="condition5">
  condition5 is true
 </elsif>
 <elsif condition="condition6">
  condition6 is true
 </elsif>
 <else>
  condition5,4,6 are false
 </else>

 Here we will put a table where the rows are in a while loop.
 Note that we use $a here, but $a is not defined in the calling script,
 so it stays $a. Only in the last loop of the while we will define $a.
 <while condition="func">
 $i: $a <if condition="condition7"> condition7 is true inside the while 
 loop. </if><else> condition7 is false inside the while loop. </else>
 </while> 

And template2.txt:

 --- start of template2 ---
 <comment> this is a comment in the included template </comment>
 included template 2 is here. Template 2 is inserted into the first 
 template.
 We can put use here all the variables like "$title".
 ---  end of template2  ---

The script:

 use Class::Skin;

 my $i = 777;
 my $title = "Usage of Class::Skin";

 # create new Class::Skin object
 my $skin = new Class::Skin("template1.txt");

 # read the template file 
 $skin->read();

 # parse the template and print the result. we send in anonymous 
 # hash all the variables that we would like to use in the template
 print $skin->parse({ title => $title,
		      i => $i,
		      condition1 => 1,
		      condition2 => 0,
		      condition3 => 1,
		      condition4 => 0,
		      condition5 => 1,
		      condition6 => 0,
		      condition7 => 1,
		      condition8 => 0,
		      condition9 => 1,
		      filepath   => "template2.txt",
		      func       => \&call_back_func});

 # callback function. we will give a reference to that function as 
 # one of the variables we sent with the parse method. callback 
 # functions are used with the 'while' block. this function must
 # return true or false (1 or 0). it is important that it will not 
 # return always true (or else the while block will be repeated 
 # infinite times).
 sub call_back_func {
    my $variables = shift; # always a reference to the anonymous hash
    # that we send with the parse method is 
    # send to the callback functions.
    
    $variables->{i}++; # increment the value of i
    
    # toggle the value of condition9 according to the value of i
    if ($variables->{i} == 779) {
        $variables->{condition9} = 1;
    }
    else {
        $variables->{condition9} = 0;
    }
    
    if ($variables->{i} == 780) {
        $variables->{a} = "[this is a]";
    }
    else {
        $variables->{a} = undef;
    }

    # return true unless i > 780. 
    if ($variables->{i} <= 780) {
        return 1;
    }
    else {
        return 0;
    }
 } # of call_back_func 


The result:

 *** Usage of Class::Skin ***

 Here two templates will be included:

 --- start of template2 ---

 included template 2 is here. Template 2 is inserted into the first 
 template.
 We can put use here all the variables like "Usage of Class::Skin".
 ---  end of template2  ---
 --- start of template2 ---

 included template 2 is here. Template 2 is inserted into the first 
 template.
 We can put use here all the variables like "Usage of Class::Skin".
 ---  end of template2  ---

 Here we will put some conditions:

  condition1 is true
   condition3 is true (in condition1)
    condition5 is true (in condition3 that in condition1)
  condition5 is true
 Here we will put a table where the rows are in a while loop.
 Note that we use $a here, but $a is not defined in the calling script, 
 so it stays $a. Only in the last loop of the while we will define $a.

 778: $a  condition7 is true inside the while loop.  

 779: $a  condition7 is true inside the while loop. 

 780: [this is a]  condition7 is true inside the while loop. 



=head1 AUTHOR

Rani Pinchuk, rani@cpan.org

=head1 COPYRIGHT

Copyright (c) 2002 Ockham Technology N.V. & Rani Pinchuk. 
All rights reserved.  
This package is free software; you can redistribute it and/or modify 
it under the same terms as Perl itself.

=head1 SEE ALSO

L<Log::NullLogLite(3)>,
L<Log::LogLite(3)>