<?PHP
/**
 * Base class for patTemplate readers
 *
 * $Id: Reader.php,v 1.57 2004/09/07 19:09:56 schst Exp $
 *
 * This class is able to parse patTemplate tags from any string you hand it over
 * It will emulate some kind of SAX parsing by calling start-, end- and CData-handlers.
 *
 * @package        patTemplate
 * @subpackage    Readers
 * @author        Stephan Schmidt <schst@php.net>
 */
/**
 * No input
 */
define'PATTEMPLATE_READER_ERROR_NO_INPUT'6000 );

/**
 * Unknown tag
 */
define'PATTEMPLATE_READER_ERROR_UNKNOWN_TAG'6001 );

/**
 * Invalid tag (missing attribute)
 */
define'PATTEMPLATE_READER_ERROR_INVALID_TAG'6002 );

/**
 * Closing tag is missing
 */
define'PATTEMPLATE_READER_ERROR_NO_CLOSING_TAG'6003 );
 
/**
 * Invalid closing tag
 */
define'PATTEMPLATE_READER_ERROR_INVALID_CLOSING_TAG'6004 );

/**
 * Invalid condition specified
 */
define'PATTEMPLATE_READER_ERROR_INVALID_CONDITION'6005 );

/**
 * No name has been specified
 */
define'PATTEMPLATE_READER_ERROR_NO_NAME_SPECIFIED'6010 );

/**
 * CData in a conditional template
 */
define'PATTEMPLATE_READER_NOTICE_INVALID_CDATA_SECTION'6050 );

/**
 * template already exists
 */
define'PATTEMPLATE_READER_NOTICE_TEMPLATE_EXISTS'6051 );

/**
 * Base class for patTemplate readers
 *
 * This class is able to parse patTemplate tags from any string you hand it over
 * It will emulate some kind of SAX parsing by calling start-, end- and CData-handlers.
 *
 * @abstract
 * @package        patTemplate
 * @subpackage    Readers
 * @author        Stephan Schmidt <schst@php.net>
 */
class patTemplate_Reader extends patTemplate_Module
{
   
/**
    * reference to the patTemplate object that instantiated the module
    *
    * @access    protected
    * @var    object
    */
    
var    $_tmpl;

   
/**
    * stack for all open elements
    * @access    private
    * @var    array
    */
    
var    $_elStack;

   
/**
    * stack for all open templates
    * @access    private
    * @var    array
    */
    
var    $_tmplStack;

   
/**
    * character data
    * @access    private
    * @var    array
    */
    
var    $_data;

   
/**
    * tag depth
    * @access    private
    * @var    integer
    */
    
var    $_depth;

   
/**
       * templates that have been found
    * @access    protected
    * @var        array
    */
    
var $_templates    =    array();
    
   
/**
       * path to the template
    * @access    protected
    * @var        array
    */
    
var $_path    =    array();
    
   
/**
    * start tag for variables
    * @access    private
    * @var        string
    */
    
var    $_startTag;
    
   
/**
    * end tag for variables
    * @access    private
    * @var        string
    */
    
var    $_endTag;
    
   
/**
    * default attributes
    *
    * @access    private
    * @var        array
    */
    
var    $_defaultAtts    =    array();

   
/**
    * root attributes
    *
    * This is used when reading the template content
    * from an external file.
    *
    * @access    private
    * @var        array
    */
    
var    $_rootAtts    =    array();

   
/**
    * inherit attributes
    *
    * @access    private
    * @var        array
    */
    
var    $_inheritAtts    =    array();

   
/**
    * name of the first template that has been found
    *
    * @access    private
    * @var        string
    */
    
var    $_root null;

   
/**
    * all data that has been processed
    *
    * @access    private
    * @var        string
    */
    
var    $_processedData null;

   
/**
    * current input
    *
    * @access    private
    * @var        string
    */
    
var    $_currentInput null;

   
/**
    * all loaded functions
    *
    * @access    private
    * @var        array
    */
    
var    $_functions    =    array();

   
/**
    * function aliases
    *
    * @access   private
    * @var      array
    */
    
var $_funcAliases = array();
    
   
/**
    * set a reference to the patTemplate object that instantiated the reader
    *
    * @access    public
    * @param    object        patTemplate object
    */
    
function setTemplateReference( &$tmpl )
    {
        
$this->_tmpl        =    &$tmpl;
    }

   
/**
    * read templates from any input 
    *
    * @abstract    must be implemented in the template readers
    * @param    mixed    input to read from.
    *                    This can be a string, a filename, a resource or whatever the derived class needs to read from
    * @param    array    options, not implemented in current versions, but future versions will allow passing of options
    * @return    array    template structure
    */
    
function readTemplates$input$options = array() )
    {
        return    array();
    }

   
/**
    * load template from any input 
    *
    * If the a template is loaded, the content will not get
    * analyzed but the whole content is returned as a string.
    *
    * @abstract    must be implemented in the template readers
    * @param    mixed    input to load from.
    *                    This can be a string, a filename, a resource or whatever the derived class needs to read from
    * @param    array    options, not implemented in current versions, but future versions will allow passing of options
    * @return    string  template content
    */
    
function loadTemplate$input$options = array() )
    {
        return    
$input;
    }


   
/**
    * set options
    *
    * @access    public
    * @param    array    array containing options
    */
    
function setOptions$options )
    {
        
$this->_startTag    =    $options['startTag'];
        
$this->_endTag        =    $options['endTag'];

        
$this->_options        =    $options;
        
        if (isset(
$options['functionAliases']))
        {
            
$this->_funcAliases $options['functionAliases'];
        }
        
array_map('strtolower'$this->_funcAliases);
    }

   
/**
    * add an alias for a function
    *
    * @access   public
    * @param    string  alias
    * @param    string  function name
    */
    
function addFunctionAlias($alias$function)
    {
        
$this->_funcAliases[strtolower($alias)] = $function;
    }
    
   
/**
    * set the root attributes
    *
    * @access    public
    * @param    array    array containing options
    */
    
function setRootAttributes$attributes )
    {
        
$this->_rootAtts $attributes;
    }

   
/**
    * parse templates from string
    *
    * @access    private
    * @param    string        string to parse
    * @return    array        templates
    */
    
function parseString$string )
    {
        
/**
         * apply input filter before parsing
         */
        
$string $this->_tmpl->applyInputFilters$string );
        
        
$this->_inheritAtts    =    array();
        
$this->_elStack        =    array();
        
$this->_data        =    array( '' );
        
$this->_tmplStack    =    array();
        
$this->_depth        =    0;
        
$this->_templates    =    array();
        
$this->_path        =    array();
        
$this->_processedData    =    '';
        
        
$this->_defaultAtts    =    $this->_tmpl->getDefaultAttributes();
        
        if( !isset( 
$this->_defaultAtts['autoload'] ) )
            
$this->_defaultAtts['autoload']    =    'on';

        
/**
         * create a special root template
         */
        
$attributes        $this->_rootAtts;
        
$attributes['name']    = '__ptroot';
        
        
$rootTemplate    $this->_initTemplate$attributes );
        unset( 
$rootTemplate['isRoot'] );
        
$this->_root        =    null;

        
array_push$this->_tmplStack$rootTemplate );

        
/**
         *start parsing
         */        
        
$patNamespace    =    strtolower$this->_tmpl->getNamespace() );

        
$regexp    =    '/(<(\/?)([[:alnum:]]+):([[:alnum:]]+)[[:space:]]*([^>]*)>)/im';

        
$tokens    =    preg_split$regexp$string, -1PREG_SPLIT_DELIM_CAPTURE );

        
/**
         * the first token is always character data
         * Though it could just be empty
         */
        
if( $tokens[0] != '' )
            
$this->_characterData$tokens[0] );

        
$cnt    =    count$tokens );
        
$i        =    1;
        
// process all tokens
        
while( $i $cnt )
        {
            
$fullTag    =    $tokens[$i++];
            
$closing    =    $tokens[$i++];
            
$namespace    =    strtolower$tokens[$i++] );
            
$tagname    =    strtolower$tokens[$i++] );
            
$attString    =    $tokens[$i++];
            
$empty        =    substr$attString, -);
            
$data        =    $tokens[$i++];

            
/**
             * check, whether it's a known namespace
             * currently only the template namespace is possible
             */
             
if( $patNamespace != $namespace )
             {
                 
$this->_characterData$fullTag );
                 
$this->_characterData$data );
                continue;
             }

            
/**
             * is it a closing tag?
             */
            
if( $closing === '/' )
            {
                
$result    =    $this->_endElement$namespace$tagname );
                if( 
patErrorManager::isError$result ) )
                {
                    return    
$result;
                }
                
$this->_characterData$data );
                continue;
            }

            
/**
             * Is empty or opening tag!
             */
            
if( $empty === '/' )
                
$attString    =    substr$attString0, -);

            
$attributes    =    $this->_parseAttributes$attString );
            
$result     =    $this->_startElement$namespace$tagname$attributes );
            if( 
patErrorManager::isError$result ) )
            {
                return    
$result;
            }

            
/**
             * check, if the tag is empty
             */
            
if( $empty === '/' )
            {
                
$result    =    $this->_endElement$namespace$tagname );
                if( 
patErrorManager::isError$result ) )
                {
                    return    
$result;
                }
            }

            
$this->_characterData$data );
        }

        
$rootTemplate array_pop$this->_tmplStack );

        
$this->_closeTemplate$rootTemplate$this->_data[0] );
        
        
/**
         * check for tags that are still open
         */
        
if( $this->_depth )
        {
            
$el    =    array_pop$this->_elStack );
            return 
patErrorManager::raiseError(
                
PATTEMPLATE_READER_ERROR_NO_CLOSING_TAG,
                
$this->_createErrorMessage"No closing tag for {$el['ns']}:{$el['name']} found" )
            );
        }
        
        return    
$this->_templates;
    }
    
   
/**
    * parse an attribute string and build an array
    *
    * @access    private
    * @param    string    attribute string
    * @param    array    attribute array
    */
    
function _parseAttributes$string )
    {
         static 
$entities = array(
                                    
'&lt;' => '<',
                                    
'&gt;' => '>',
                                    
'&amp;' => '&',
                                    
'&quot;' => '"',
                                    
'&apos;' => '\''
                                
);

        
$attributes    =    array();
        
preg_match_all('/([a-zA-Z_0-9]+)="((?:\\\.|[^"\\\])*)"/U'$string$match);
        for (
$i 0$i count($match[1]); $i++)
        {
            
            
$attributes[strtolower$match[1][$i] )] = strtr( (string)$match[2][$i], $entities );
        }
        return    
$attributes;
    }

   
/**
    * handle start element
    *
    * @access    private
    * @param    string        element name
    * @param    array        attributes
    */
    
function _startElement$ns$name$attributes )
    {
        
array_push$this->_elStack, array(
                                            
'ns'            =>  $ns,
                                            
'name'            =>    $name,
                                            
'attributes'    =>    $attributes,
                                        )
                 );

        
$this->_depth++;

        
$this->_data[$this->_depth]    =    '';

        
/**
         * handle tag
         */
        
switch( $name )
        {
            
/**
             * template
             */
            
case 'tmpl':
                
$result    =    $this->_initTemplate$attributes );
                break;

            
/**
             * sub-template
             */
            
case 'sub':
                
$result    =    $this->_initSubTemplate$attributes );
                break;        

            
/**
             * link
             */
            
case 'link':
                
$result    =    $this->_initLink$attributes );
                break;        

            
/**
             * variable
             */
            
case 'var':
                
$result    =    false;
                break;    

            
/**
             * instance
             */
            
case 'instance':
            case 
'comment':
                
$result    =    false;
                break;    

            
/**
             * any other tag
             */
            
default:
                if (isset(
$this->_funcAliases[strtolower($name)])) {
                    
$name $this->_funcAliases[strtolower($name)];
                }
                
$name ucfirst$name );
                if( !
$this->_tmpl->moduleExists'Function'$name ) )
                {
                    return 
patErrorManager::raiseError(
                                                        
PATTEMPLATE_READER_ERROR_UNKNOWN_TAG,
                                                        
$this->_createErrorMessage"Unknown tag {$ns}:{$name}." )
                                                    );
                }
                
$result = array(
                                
'type'       => 'custom',
                                
'function'   => $name,
                                
'attributes' => $attributes
                                
);
                break;
        }

        if( 
patErrorManager::isError$result ) )
        {
            return    
$result;
        }
        
        
array_push$this->_tmplStack$result );
    }

   
/**
    * handle end element
    *
    * @access    private
    * @param    string        element name
    */
    
function _endElement$ns$name )
    {
        
$el            =    array_pop$this->_elStack );
        
$data        =    $this->_getCData();
        
$this->_depth--;

        if( 
$el['name'] != $name || $el['ns'] != $ns )
        {
            return 
patErrorManager::raiseError(
                
PATTEMPLATE_READER_ERROR_INVALID_CLOSING_TAG,
                
$this->_createErrorMessage"Invalid closing tag {$ns}:{$name}, {$el['ns']}:{$el['name']} expected" )
            );
        }
        
        
$tmpl    =    array_pop$this->_tmplStack );

        
/**
         * handle tag
         */
        
switch( $name )
        {
            
/**
             * template
             */
            
case 'tmpl':
                
$this->_closeTemplate$tmpl$data );
                break;

            
/**
             * sub-template
             */
            
case 'sub':
                
$this->_closeSubTemplate$tmpl$data );
                break;

            
/**
             * link
             */
            
case 'link':
                
$this->_closeLink$tmpl );
                break;

            
/**
             * variable
             */
            
case 'var':
                
$this->_handleVariable$el['attributes'], $data );
                break;

            
/**
             * instance
             */
            
case 'instance':
                break;

            
/**
             * comment
             */
            
case 'comment':
                
$this->_handleComment$el['attributes'], $data );
                break;    

            
/**
             * custom function
             */
            
default:
                if (isset(
$this->_funcAliases[strtolower($name)])) {
                    
$name $this->_funcAliases[strtolower($name)];
                }

                
$name ucfirst$name );
                if( !isset( 
$this->_functions[$name] ) )
                {
                    
$this->_functions[$name] = $this->_tmpl->loadModule'Function'$name );
                    
$this->_functions[$name]->setReader$this );
                }
                
                
$result $this->_functions[$name]->call$tmpl['attributes'], $data );

                if( 
patErrorManager::isError$result ) )
                {
                    return 
$result;
                }
                
                if( 
is_string$result ) )
                    
$this->_characterData$resultfalse );
                
                break;
        }
    }

   
/**
    * handle character data
    *
    * @access    private
    * @param    string        data
    */
    
function _characterData$data$readFromTemplate true )
    {
        
$this->_data[$this->_depth]    .=    $data;

        if( 
$readFromTemplate )
            
$this->_processedData .= $data;

        return    
true;
    }
    
   
/**
    * handle a Link
    *
    * @access    private
    * @param    array        attributes
    * @return    boolean        true on success
    */
    
function _initLink$attributes )
    {
        
/**
         * needs a src attribute
         */
        
if( !isset( $attributes['src'] ) )
        {
            return 
patErrorManager::raiseError(
                                                
PATTEMPLATE_READER_ERROR_INVALID_TAG,
                                                
$this->_createErrorMessage"Attribute 'src' missing for link" )
                                                );
        }
        
        
/**
         * create a new template
         */
        
$tmpl    =    array(
                            
'type'            =>    'link',
                            
'src'            =>    $attributes['src'],
                        );
        return 
$tmpl;
    }

   
/**
    * close a link template
    *
    * It will be added to the dependecies of the parent template.
    *
    * @access    private
    * @param    array    template definition for the link
    */
    
function _closeLink$tmpl )
    {
        
/**
         * add it to the dependencies
         */
        
if( !empty( $this->_tmplStack ) )
        {
            
$this->_addToParentTag'dependencies',
                                          
strtolower$tmpl['src'] )
                                        );
            
$this->_characterDatasprintf"%sTMPL:%s%s"$this->_startTagstrtoupper$tmpl['src'] ), $this->_endTag ) );
        }

        return 
true;    
    }
    
   
/**
    * create a new template
    *
    * @access    private
    * @param    array        attributes
    * @return    boolean        true on success
    */
    
function _initTemplate$attributes )
    {
        
/**
         * build name for the template
         */
        
if( !isset( $attributes['name'] ) )
            
$name    =    $this->_buildTemplateName();
        else
        {
            
$name    =    strtolower$attributes['name'] );
            unset( 
$attributes['name'] );
        }
        
        
/**
         * name must be unique
         */
        
if( isset( $this->_templates[$name] ) || $this->_tmpl->exists$name ) )
        {
            
patErrorManager::raiseNotice(
                                                
PATTEMPLATE_READER_NOTICE_TEMPLATE_EXISTS,
                                                
$this->_createErrorMessage"Template $name already exists" ),
                                                
$name
                                            
);
        }

        
/**
         * update the path
         */
        
array_push$this->_path$name );

        if( isset( 
$attributes['maxloop'] ) )
        {
            if( !isset( 
$attributes['parent'] ) )
                
$attributes['parent'] = $this->_getFromParentTemplate'name' );
        }

        
$attributes    =    $this->_prepareTmplAttributes$attributes$name );

        
array_push$this->_inheritAtts, array(
                                                
'whitespace' => $attributes['whitespace'],
                                                
'unusedvars' => $attributes['unusedvars'],
                                                
'autoclear'  => $attributes['autoclear']
                                            )
                 );
        
        
/**
         * create a new template
         */
        
$tmpl    =    array(
                            
'type'            =>    'tmpl',
                            
'name'            =>    $name,
                            
'attributes'    =>    $attributes,
                            
'content'        =>    '',
                            
'dependencies'    =>    array(),
                            
'varspecs'        =>    array(),
                            
'comments'        =>    array(),
                            
'loaded'        =>    false,
                            
'parsed'        =>    false,
                            
'input'            =>  $this->_name.'://'.$this->_currentInput
                        
);

        if( 
$this->_root == null )
        {
            
$this->_root $name;
            
$tmpl['isRoot'] = true;
        }


        
/**
         * prepare subtemplates
         */
        
switch( $attributes['type'] )
        {
            case 
'condition':
            case 
'modulo':
                
$tmpl['subtemplates']    =    array();
                break;
        }

        return 
$tmpl;
    }

   
/**
    * prepare attributes
    *
    * @access    private
    * @param    array    attributes
    * @param    string    template name (only used for error messages)
    * @return    array    attributes
    */
    
function _prepareTmplAttributes$attributes$templatename )
    {
        
/**
         * do not prepare twice
         */
        
if( isset( $attributes['__prepared'] ) && $attributes['__prepared'] === true )
            return 
$attributes;
            
        
$attributes    =    $this->_inheritAttributes$attributes );

        
/**
         * get the attributes
         */
        
$attributes    =    array_merge$this->_tmpl->getDefaultAttributes(), $attributes );

        
$attributes['type']    =    strtolower$attributes['type'] );

        
        if( !isset( 
$attributes['addsystemvars'] ) )
        {
        
$attributes['addsystemvars'] = false;
        }
        else
        {
            switch (
$attributes['addsystemvars'])
            {
                case 
'on':
                case 
'boolean':
                    
$attributes['addsystemvars'] = 'boolean';
                    break;
                case 
'int':
                case </