<?php
/* vim: set expandtab tabstop=4 shiftwidth=4: */
// +----------------------------------------------------------------------+
// | PHP version 4.0                                                      |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997, 1998, 1999, 2000, 2001 The PHP Group             |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.0 of the PHP license,       |
// | that is bundled with this package in the file LICENSE, and is        |
// | available at through the world-wide-web at                           |
// | http://www.php.net/license/2_02.txt.                                 |
// | If you did not receive a copy of the PHP license and are unable to   |
// | obtain it through the world-wide-web, please send a note to          |
// | license@php.net so we can mail you a copy immediately.               |
// +----------------------------------------------------------------------+
// | Authors: Naoki Shima <naoki@avantexchange.com>                       |
// |                                                                      |
// +----------------------------------------------------------------------+
//
// $Id$

require_once 'PEAR.php';

/**
* todo:
*       parse template once, store and use parsed template
*       php generated template support - use ob_* function
*       documentation
*       variable declaration
*       error code CONSTANTS
*       error code to err text mapping
*       ALT BLOCK support for multi-depth loop
*       error handling
*       argument checking
*       example
*       Utilize Cache from PEAR
*/

define('ALT', 'ALT');
define('END', '_END');
define('START', '_START');

class
AvanTemplate extends PEAR
{
    
// {{{ properties

    /**
     * location or directory where template files reside
     * @var    string
     * @access private
     */
    
var $_location;

    
/**
     * contents of template file being parsed
     * @var    string
     * @access private
     */
    
var $_working;

    
/**
     * Which block to show, or hide
     * @var    array
     * @access private
     */
    
var $_block;

    
// }}}
    // {{{ constructor

    /**
     * Initialize variable used by this class
     *
     * @param  string   (optional) root directory of the template file(s)
     *                  defaults to the same directory of the calling php script
     * @param  boolean  (optional) if string is needed to be handled multi-byte
     *                  safe. defaults to false
     * @access public
     * @return void
     */
    
function AvanTemplate($dir = '', $multi_byte = false)
    {
        
$this->_hide_unset = true;
        
$this->_prepend = '[%';
        
$this->_append = '%]';
        
$this->_cs = array('BLOCK_','LOOP_');
        
$this->_idx = '1';
        
$this->_location = $dir;
        
$this->_multi_byte = $multi_byte;
        
$this->PEAR();
    }

    
// }}}
    // {{{ destructor

    /**
     * Does nothing right now
     *
     * @access public
     * @return void
     */
    
function _AvanTemplate()
    {
        
$this->_PEAR();
    }
  
    
// }}}
    // {{{ showUnset()

    /**
     * Show unset entity of template in finished content
     *
     * @access public
     * @return void
     */
    
function showUnset()
    {
        
$this->_hideUnset = false;
    }

    
// }}}
    // {{{ hideBlock()

    /**
     * Hide named block from finished content
     *
     * @param  string   Name of the block to hide
     * @access public
     * @return void
     */
    
function hideBlock($name)
    {
        
$this->_block[$name] = false;
    }

    
// }}}
    // {{{ showBlock()

    /**
     * Show named block from finished content
     *
     * @param  string   Name of the block to show
     * @access public
     * @return void
     */
    
function showBlock($name)
    {
        
$this->_block[$name] = true;
    }

    
// }}}
    // {{{ _generateUniqueId()

    /**
     * Generate random unique number
     *
     * @access private
     * @return int
     */
    
function _generateUniqueId()
    {
        return
$this->_idx++;
    }
   
    
// }}}
    // {{{ _getParentName()
   
    /**
     * Find name of the Parent entity
     *
     * @param  int       ID of entity
     * @access private
     * @return string
     */
    
function _getParentName($id)
    {
        return
$this->_parent_name[$id];
    }
  
    
// }}}
    // {{{ setChildValue()

    /**
     * Set value to the child of entity
     *
     * @param  int       Id of the parent entity
     * @param  string    Name of the entity
     * @param  mixed     Value to be set
     * @param  boolean   (optional) Whether to append the value to the existing
     *                   value or override
     * @access public
     * @return boolean   TRUE for success and FALSE for fail
     */
    
function setChildValue($parent_id, $name, $value, $append = false)
    {
        if(!
$this->_validateArray(&$value) && !$name) {
            return
FALSE;
        }
        
$id = $this->_generateUniqueId();
        
$parent_name = $this->_getParentName($parent_id);
        if(
$this->_argument[$parent_name][$parent_id][$name] && $append) {
            
$this->_argument[$parent_name][$parent_id][$name][$id] = $value;
        } else {
            
$this->_argument[$parent_name][$parent_id][$name] =
                array(
$id => $value);
        }
        return
$id;
    }

    
// }}}
    // {{{ setValue()

    /**
     * Set value for variable interporation
     *
     * @param : string   name of the variable to replace
     * @param : mixed    value to replace the variable
     * @param : boolean  (optional) whether to replace or append the value
     *
     * @return: integer  ID of the entry. False is returned when params
     *                   are invalid
     * @access: public
     */
    
function setValue($name, $value, $append = false)
    {
        
$id = TRUE;
        
$this->_validateArray(&$value);
        if(
$name && $value) {
            if(
is_array($value)) {
                
$id = $this->_generateUniqueId();
                if(
$this->_argument[$name] && $append) {
                    
$this->_argument[$name][$id] = $value;
                } else {
                    
$this->_argument[$name] = array($id => $value);
                }
                
$this->_parent_name[$id] = $name;
            } else {
                
$this->_argument[$name] = $value;
            }
            return
$id;
        } else {
            return
FALSE;
        }
    }
    
    
// }}}
    // {{{ setList()

    /**
     * under development
     *
     *
     * @return: void
     * @access: public
     */
    
function setList($name,$list)
    {
        if(
$name && is_array($list)) {
            
$this->_argument[$name] = $list;
        }
    }

    
// }}}
    // {{{ setList()

    /*
     * under development
     */
    
function setValues($list)
    {
        if(
is_array($list)) {
            foreach(
$list AS $key => $value) {
                
$this->setValue($key,$value);
            }
        }
    }

    
// }}}
    // {{{ setAppend()

    /**
     * Change append string. This method returns current append.
     *
     * @param : string
     *
     * @return: string
     * @access: public
     */
    
function setAppend($new)
    {
        
$old = $this->_append;
        
$this->_append = $new;
        return
$old;
    }

    
// }}}
    // {{{ setPrepend()

    /**
     * Change prepend string. This method returns current prepend.
     *
     * @param : string
     *
     * @return: string
     * @access: public
     */
    
function setPrepend($new)
    {
        
$old = $this->_prepend;
        
$this->_prepend = $new;
        return
$old;
    }

    
// }}}
    // {{{ format()

    /**
     * @access: public
     */
    
function format($file, $vals = '')
    {
        
$is_vals = $this->_validateArray(&$vals);
        if(
$this->_argument) {
            if(
$is_vals) {
                
// merge them if both exists
                
$this->_argument = array_merge_recursive($vals,$this->_argument);
            }
        } elseif(!
$is_vals) {
            
// none set, so raise error
            //return $this->raiseError($errstr, $errno);
            
return;
        } else {
            
// only $vals exists, then assign it to $this->_argument
            
$this->_argument = $vals;
        }
        
$this->_loadTemplate($file);
        
$this->_loadExternalTemplate();
        
$this->_apply();
        
$this->_updateContents();
        return
$this->_getContents();
    }
    
    
/*
     * Cast parameter $val to array if it is an object, and then checks if
     * $val is an array. If it is not, returns FALSE.
     * Otherwise, this returns TRUE;
     */
    
function _validateArray(&$val)
    {
        if(
is_object($val)) {
            
$val = (array) $val;
        }
        if(
is_array($val)) {
            return
true;
        } else {
            return
FALSE;
        }
    }

    
// }}}

    /**
     * Sprit string...
     * @access: private
     */
    
function _split($string, $prepend, $append, $offset = '0')
    {
        
$length = $this->_strlen($prepend);
        
$append_length = $this->_strlen($append);
        if(
$pos = $this->_strpos($string,$prepend,$offset)){
            
$result['head'] = $this->_substr($string, $start, $pos);
            
$end = $pos+$length;
            
$pos = $this->_strpos($string, $append, $pos);
            
$result['inside'] = $this->_substr($string, $end ,($pos-$end));
            
$end = $pos+$append_length;
            
$result['tail'] = $this->_substr($string, $end);
            
$result['pos'] = $end;
            return
$result;
        }
        return
FALSE;
    }

    
/**
     * Look for include statement in template file and substitute
     * include statement with the contents of template file specified.
     *
     * @return void
     * @access private
     */
    
function _loadExternalTemplate()
    {
        
$string = $this->_working;
        
// load file whose name specified in template
        
$prepend = $this->_prepend.'INCLUDE_';
        while(
$result = $this->_split($string,$prepend,$this->_append,$offset)){
            
$offset = $result['pos'];
            
$tmp = $this->_readFile($this->_location.$result['inside']);
            
$string = $result['head'].$tmp.$result['tail'];
        }
        
// load file whose name specified by includeFile()
        
if($this->_includeFiles) {
            foreach(
$this->_includeFiles AS $handle => $name) {
                
$needle = $this->_prepend.'INCLUDE:'.$handle.$this->_append;
                
$length = $this->_strlen($needle);
                while(
$pos = $this->_strpos($string,$needle)){
                    
$tmp = $this->_readFile($this->_location.$name);
                    
$string = $this->_substr($string,0,$pos).$tmp.$this->_substr($string,($pos+$length));
                }
            }
        }
        
$this->_setWorking($string);
    }

    
/**
     * Update contents(result) with processed template.
     * Final processing is done in this function.
     *
     * @return void
     * @access private
     */
    
function _updateContents()
    {
        if(
$this->_hide_unset){
            
$this->_hideUnsetEntity();
        }
        
$this->_setContents($this->_working);
    }

    function
_getAltBlock($string)
    {
        
$alt = $this->_prepend.ALT.$this->_append;
        
$alt_length = $this->_strlen($alt);
        if(
$alt_pos = $this->_strpos($string,$alt)) {
            
$alt_start = $alt_pos + $alt_length;
            return
$this->_substr($string,$alt_start);
        }
        return
FALSE;
    }

    function
_hideUnsetEntity()
    {
        
$string = $this->_working;
        
$append = START.$this->_append;
        foreach(
$this->_cs AS $val) {
            
$prepend = $this->_prepend.$val;
            while(
$tmp = $this->_split($string,$prepend,$append)){
                
$name = $tmp['inside']; //name of block
                
$needle = $prepend.$name.END.$this->_append;
                
$pos = $this->_strpos($string,$needle,$tmp['pos']);
                
$offset = $pos + $this->_strlen($needle);
                
$str = $this->_substr($string,$tmp['pos'],($pos-$tmp['pos']));
                
$alt_str = $this->_getAltBlock($str);
                
$string = $tmp['head'].$alt_str.$this->_substr($string, $offset);
            }
        }
// end foreach
        
$offset = 0;
        while(
$pos = $this->_strpos($string, $this->_prepend, $offset)) {
            
$head = $this->_substr($string, 0, $pos);
            
$pos = $this->_strpos($string, $this->_append, $pos);
            
$length = $this->_strlen($this->_append);
            
$tail = substr($string, ($pos+$length));
            
$offset = $pos;
            
$string = $head.$tail;
        }
        
$this->_setWorking($string);
    }

    function
_apply()
    {
        if(
is_array($this->_block)) {
            foreach(
$this->_block AS $key => $value) {
                
$this->_formatBlock($key,$value);
            }
        }
        foreach(
$this->_argument AS $key => $value) {
            if(
is_array($value)) {
                
$this->_setWorking($this->_formatLoop($key,$value));
            } else {
                
$needle = $this->_prepend.$key.$this->_append;
                
$this->_setWorking($this->_strReplace($needle, $value));
            }
        }
    }
    
    
/**
     * Process BLOCK
     *
     * @param : string   Name of the Block
     * @param : boolean  Whether to show or hide the BLOCK
     *
     * @return: void
     * @access: private
     */
    
function _formatBlock($name, $is_shown)
    {
        while(
$arr = $this->_getControlStructure($name,'BLOCK')){
            if(
$is_shown) {
                
$arr['inside'] = $this->_stripAltBlock($arr['inside']);
            } else {
                
$arr['inside'] = $this->_getAltBlock($arr['inside']);
            }
            
$this->_setWorking($arr['head'].$arr['inside'].$arr['tail']);
        }
    }

    
/**
     * Process LOOP
     *
     * @param : string   Name of the loop
     * @param : mixed    Value to replace(interporate) the variable
     * @param : string   (optional) string in which it looks for LOOP
     *
     * @return: string   Processed string
     * @access: private
     */
    
function _formatLoop($item, $vals, $string='')
    {
        if(!
$string) {
            
$string = $this->_working;
        }
        while(
$arr = $this->_getControlStructure($item,'LOOP',$string)){
            unset(
$result);
            foreach(
$vals AS $val) {
                if(
$this->_validateArray(&$val)) {
                    unset(
$tmp);
                    foreach(
$val AS $key => $value) {
                        if(!
$tmp) {
                            
$tmp = $arr['inside'];
                        }
                        if(
is_array($value)) {
                            
$tmp = $this->_formatLoop($item.'.'.$key,$value,$tmp);
                            continue
1;
                        }
                        
$needle = $this->_prepend.$item.'.'.$key.$this->_append;
                        
$tmp = $this->_stripAltBlock($tmp);
                        
$tmp = $this->_strReplace($needle,$value,$tmp);
                    }
                    
$result .= $tmp;
                }
            }
            
$string = $arr['head'].$result.$arr['tail'];
        }
        return
$string;
    }

    
/**
     * Strip ALT BLOCK from passed string
     * @access: private
     */
    
function _stripAltBlock($string)
    {
        
$alt = $this->_prepend.ALT.$this->_append;
        
$pos = $this->_strrpos($string, END.$this->_append);
        if((
$alt_pos = $this->_strrpos($string,$alt)) && (!$pos || ($pos && ($pos < $alt_pos)))) {
            return
$this->_substr($string,0,$alt_pos);
        }
        return
$string;
    }

    function
_setWorking($val)
    {
        
$old = $this->_working;
        
$this->_working = $val;
        return
$old;
    }

    function
_getControlStructure($item,$type,$string = '')
    {
        if(!
$string) {
            
$string = $this->_working;
        }
        
$needle = $this->_prepend.$type.'_'.$item.START.$this->_append;
        
$length = $this->_strlen($needle);
        if(
$pos = $this->_strpos($string,$needle)){
            
$result['head'] = $this->_substr($string, 0, $pos);
            
$end = $pos+$length;
            
$needle = $this->_prepend.$type.'_'.$item.END.$this->_append;
            
$pos = $this->_strpos($string, $needle, $pos);
            
$result['inside'] = $this->_substr($string, $end ,($pos-$end));
            
$end = $pos+$this->_strlen($needle);
            
$result['tail'] = $this->_substr($string, $end);
            return
$result;
        } else {
            return
FALSE;
        }
    }

    
/**
     * Get contents stored in the object
     *
     * @access private
     * @return string
     */
    
function _getContents()
    {
        if(!
$this->_contents) {
            return;
            
// return $this->raiseError($errstr, $errno);
        
}
        return
$this->_contents;
    }

    
/**
     * Wrapper function for str_replace(). Multi-byte safe.
     * Exactly the same API as str_replace()
     * Refer manual for str_replace()
     * @param  mixed
     * @param  mixed  
     * @param  mixed
     *
     * @access private
     * @return mixed
     */
    
function _strReplace($search, $replace, $subject = '')
    {
        if(!
$subject) {
            
$subject = $this->_working;
        }
        if(!
$this->_multi_byte) {
            return
str_replace($search, $replace, $subject);
        }
        if(
is_array($subject)) {
            foreach(
$subject AS $sub) {
                if(
is_array($search)) {
                    for(
$i=0; count($search) >$i; $i++) {
                        
$s = $search[$i];
                        if(
is_array($replace)) {
                            
$r = $replace[$i];
                        } else {
                            
$r = $replace;
                        }
                        
$result[] = $this->_strReplaceLtd($s,$r,$sub);
                    }
                } else {
                    
$result[] = $this->_strReplaceLtd($search,$replace,$sub);
                }
            }
        } else {
            
$result = $this->_strReplaceLtd($search,$replace,$subject);
        }
        return
$result;
    }

    
/*
     * only used by _strReplace
     * @access private
     */
    
function _strReplaceLtd($search, $replace, $subject)
    {
        
$offset = 0;
        if(!(
$length = @mb_strlen($search))){
            return;
            
// return $this->raiseError($errstr, $errno);
        
}
        while(
$pos = @mb_strpos($subject,$search,$offset)) {
            
$end = $pos+$length;
            
$subject = mb_substr($subject, 0, $pos).$replace
                       
.$this->_substr($subject,$end);
            
$offset = $end;
        }
        return
$subject;
    }

    
//wrapper function -- for multi_byte
    
function _strpos($string,$needle,$offset = 0)
    {
        if(
$this->_multi_byte) {
            return @
mb_strpos($string, $needle, $offset);
        }
        return @
strpos($string, $needle, $offset);
    }

    
/**
     * Wraps strrpos function so that it calls mb_strrpos when multi-byte is
     * specified and handle string as its needle. PHP native strrpos() function
     * accepts only single character as needle, but this function can handle
     * string as a needle.
     *
     * @param : string  Heystack
     * @param : string  Needle
     *
     * @return: int     
     * @access: private
     */
    
function _strrpos($heystack, $needle)
    {
        if(
$this->_multi_byte) {
            return @
mb_strrpos($heystack, $needle);
        }
        
$tmp = 0;
        while(
$pos = strpos($heystack, $needle, $tmp)) {
            
$tmp = $pos + 1;
        }
        return (
$tmp - 1);
    }

    
//wrapper function -- for multi_byte
    
function _strlen($value)
    {
        if(
$this->_multi_byte) {
            return @
mb_strlen($value);
        }
        return
strlen($value);
    }

    
//wrapper function -- for multi_byte
    
function _substr($string, $start, $length = '')
    {
        if(
$this->_multi_byte) {
            if(
$length) {
                return
mb_substr($string,$start,$length);
            }
            return
mb_substr($string,$start);
        } else {
            if(
$length) {
                return
substr($string,$start,$length);
            }
            return
substr($string,$start);
        }
    }
  
    
/**
     * Load template file
     *
     * @param : string    Name of the file to include
     *
     * @return: void
     * @access: private
     */
    
function _loadTemplate($file)
    {
        
$this->_template = $this->_readFile($this->_location.$file);
        
$this->_setWorking($this->_template);
    }
  
    
/**
     * Get contents of file
     *
     * @param : string     Name of the file to include
     *
     * @return: string     Contents of file
     * @access: private
     */
    
function _readFile($file)
    {
        
$fp = fopen($file, 'r-');
        if(!
is_resource($fp)) {
            return;
        }
        while(
$data = fread($fp, 2048)) {
            
$contents .= $data;
        }
        
fclose($fp);
        return
$contents;
    }

    
/**
     * Save contents to return. If previously saved contents exists, then this
     * method returns the previously saved contents.
     *
     * @param : string
     *
     * @return: string
     * @access: private
     */
    
function _setContents($val)
    {
        
$old = $this->_contents;
        
$this->_contents = $val;
        return
$old;
    }
   
    
/**
     * Store name of the file to include with file handle
     *
     * @return: void
     * @access: public
     */
    
function includeFile($handle, $filename)
    {
        
$this->_includeFiles[$handle] = $filename;
    }
}

?>