<?php
//
// Edit History:
//
// Dick Munroe hide@address.com 16-Dec-2004
// Make the ':' in blocks optional.
// Allow simple single dimension arrays in loop replace.
//
// Dick Munroe hide@address.com 22-Dec-2004
// Trim \n off of blocks.
//
// Dick Munroe hide@address.com 11-Jan-2005
// Pass replacement data by reference.
// Add interfaces that allow passing data by value.
// Eliminate use of regular expression replacements as they seem to fail
// on large replacements or with many usages. There may be a leak.
//
// Dick Munroe hide@address.com 18-Jan-2005
// Remove two dimension limitation on arrays.
//
// Dick Munroe (hide@address.com) 18-Mar-2006
// Seems that is_file can generate error text when checking for a file
// in the constructor. Make it silent.
//
// Dick Munroe (hide@address.com) 20-Mar-2006
// Removal of unused tags didn't have valid regular expression.
//
// Dick Munroe (hide@address.com) 21-Mar-2006
// Fixed the end of line characters to be "unix" format.
// Turns out PREG_CAPTURE_OFFSET is only available from PHP 4.3.0 on.
// The current _replace member uses it so make the version independent
// _replace was changed to fix a memory footprint problem stumbled on
// in one of the 4.x versions so there will be a version test and
// for early version of PHP, preg_replace_all will be used.
//
// Dick Munroe (hide@address.com) 30-Nov-2006
// PHP 5 compatibility.
//
// Dick Munroe (hide@address.com) 03-Dec-2007
// Add tagged end blocks to all nested replacements.
// Add qualified replacements to blocks to make nested replacements
// work well.
//
/**
* Show erros constant
* @const SHOW_ERRORS
*/
define("SHOW_ERRORS",true);
/**
* HTML error renderer
* @const ERROR_CODE
*/
define("ERROR_CODE",'<font face="verdana, arial" size="1"><b>Layout Error: <font color="#000080">[error]</font></b></font><br>');
/**
* Read and process HTML files
*
* Template tags appear within square brackets, e.g.
*
* [date]
*
* would be a tag. Tags are replaced by the {@link IS_Layout::replace()}
* method. Normally tags are simple scaler replacements, but
* tags may also be multi dimension arrays, e.g.:
*
* [data,date]
*
* or
*
* [date,today,day]
*
* In fact, the arrays can be of any dimension and the tags that
* get replace are of the form:
*
* [tag,key0,key1,key2,...]
*
* There are two "block" oriented tokens that get used during
* template processing. The first is used to control looping
* while doing template processing of arrays. It has the form:
*
* <!-- replace id="variable name" -->
* text within which to do template processing.
* <!-- end replace -->
*
* It is possible to nest replacements by tagging the end replace
* statement:
*
* <!-- replace id="v1" -->
* <!-- replace id="v2" -->
* <!-- end replace id="v2" -->
* <!-- end replace id="v1" -->
*
* Note that the tags delimiting the block are actually html
* comments, so that the template can be editted in any html
* aware editor with no problem.
*
* Loop replacement occurs through the use of the {@link IS_Layout::loop_replace()}
* method.
*
* For both replacement methods, the arrays passed in may be
* either numeric or associative. In both cases, the natural
* binding to tag name is made, through the index whether that
* be a number or key, e.g.:
*
* [0]
*
* or
*
* [key]
*
* If a qualified end is used, then the variable replacement must be qualified by the
* block id. This is necessary to make nested replacement blocks work.
*
* @author Daniel Chaves <hide@address.com>;
* @author Dick Munroe <hide@address.com>
* @copyright copyright (c) Dick Munroe, 2004-2007, All rights reserved.
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @version 1.3
* @access public
* @package dm.IS Layout
*
* All code added or modified by Dick Munroe is being released under
* the GPL. This in no way affects or changes any prior license or
* copyright.
*
* I (Dick Munroe) attempted to contact the original author to get him to include
* my changes but got no response, which is why I started this fork.
*/
//
// Edit History:
//
// Dick Munroe hide@address.com 03-Dec-2004
// Add documentation of usage.
//
// Dick Munroe hide@address.com 14-Dec-2004
// Add member to return all tags in a file.
// Code for identifying code sections didn't
// match the example files.
//
// Dick Munroe (hide@address.com) 03-Dec-2007
// Add capabilities for nested replacement blocks.
class IS_Layout
{
/**
* Contains the html´s souce code
* @var string
* @access private
*/
VAR $_code;
/**
* Informs if the lines are set
* @var bool
* @access private
*/
VAR $_linesSet=False;
/**
* IS_Layout Constructor
* @param string $arg path to html file or the html lines
* @access public
* @return void
*/
function IS_Layout($arg=NULL)
{
if ($arg !== NULL)
{
if(@is_file($arg))
{
return $this->openFile($arg);
}
else if(is_string($arg))
{
$this->setCode($arg);
}
}
}
/**
* Opens the HTML file
* @param string $file Path to HTML file
* @return mixed boolean true, if successful or error message (see const SHOW_ERRORS)
* @access public
*/
function openFile($file)
{
$res=@fopen($file,"r");
if(!is_resource($res)){
return $this->_error('Fail to open file "'.$file.'"');
}
$code='';
while(!feof($res))
$code.=fgets($res,1024);
fclose($res);
$this->setCode($code);
return True;
}
/**
* Sets the HTML lines
* @param string $str Lines of current HTML code
* @access public
*/
function setCode($str)
{
$this->_code=$str;
$this->_linesSet=True;
}
/**
* Parse errors
* @param string $error Error Message
* @return string error message or displayed message
* @access private
* @see define SHOW_ERROR
*/
function _error($error)
{
$this->setCode(ERROR_CODE);
$this->replace('error',$error);
if(SHOW_ERRORS)
return $this->display();
else
return $this->toHtml();
}
/**
* Replace variables on HTML code
*
* @param string $sId Identification of element
* @param mixed $items [constant] String or Array of values
* @access public
*/
function replaceConstant($sid, $items)
{
$this->replace($sid, $items) ;
}
/**
* Replace variables on HTML code
*
* @param string $sId Identification of element
* @param mixed $items [reference] String or Array of values
* @access public
*/
function replace($sId, &$items)
{
if(is_array($items))
{
foreach($items as $k=>$v)
{
$this->replace("$sId,$k", $v) ;
}
}else{
$this->_doReplace($sId,$items);
}
}
/**
* Provide loop replacement for things whose values are
* constants.
*
* @see IS_Layout::loop_replace()
*/
function loop_replaceConstant($Id, $Values)
{
$this->loop_replace($Id, $Values) ;
}
/**
* Do a loop in part of HTML code replacing variables
*
* The $Values array is usually an array of arrays. In this case,
* the replacement tags are the array indices, e.g.,
*
* array(array('one', 'two'))
*
* would replace tags [0] and [1] in the loop replacement block.
* The array could also be an associative array, e.g.,
*
* array(array('a' => 'one', 'b' => 'two'))
*
* in which case the tags would be [a] and [b]. As a convenience
* arrays of scalars are allowed and are treated as if they were
* an array of arrays with only one item in the second dimension.
* For these arrays, the replaced tag is [0], always.
*
* This changes slightly if you nest replacement blocks. Inside a
* nested block, you need to qualify the replacement value as
* follows:
*
* <!-- replace id="v" -->
* [v.0]
* <!-- end replace id="v" -->
*
* [v.0] is the equivalent of an unqualified loop as [0].
*
* @param string $Id Identification of element
* @param array $Values [reference] Array of values
* @access public
*/
function loop_replace($Id, &$Values)
{
$arReturn=$this->_getElement('replace',"id=\"$Id\"");
$sReplaceLines=$arReturn[0];
$sReturn = "" ;
foreach($Values as $aValue)
{
$sTempValue=$sReplaceLines;
if (is_array($aValue))
{
foreach($aValue as $key=>$value)
{
/*
* If the block was terminated by a named end, then the
* replacement is of the form:
*
* [$id.key]
*
* instead of:
*
* [key]
*/
if ($arReturn[2])
{
$this->__replace('|' . quotemeta("[$Id.$key]") . '|s', $value, $sTempValue);
}
else
{
$this->__replace('|' . quotemeta("[$key]") . '|s', $value, $sTempValue);
}
}
}
else
{
if ($arReturn[2])
{
$this->__replace('|' . quotemeta("[$Id.0]") . '|s', $aValue, $sTempValue);
}
else
{
$this->__replace('|' . quotemeta("[0]") . '|s', $aValue, $sTempValue);
}
}
$sReturn.=$sTempValue;
}
$this->_Element_replace('replace',"id=\"$Id\"",$sReturn);
}
/**
* Get an Element of HTML Code
*
* Elements are defined as everything that appears between:
*
* <!-- $Name[:] $args -->
*
* and
*
* <!-- end $Name[:][ $args] -->
*
* If the end tag isn't named, then the nearest end un-named end tag is used.
*
* @param string $Name Identification of element
* @param mixed $args String or Array of values passed in identification
* @access private
*/
function _getElement($Name, $args=NULL)
{
if(is_array($args))
{
$comp="\s{1,}";
foreach($args as $key=>$value)
{
$comp.="$key=\"$value\"\s{0,}";
}
}
elseif ($args != NULL)
{
$comp="\s{0,}".$args;
$comp=preg_replace("[\s{1,}]","\s{1,}",$comp);
}
$return = array(NULL, NULL, TRUE);
/*
* Construct a match for a tagged end block, e.g.:
*
* <!-- replace id="foo" -->
* ...
* <!-- end replace id="foo" -->
*
* If no tagged end is found, then the nearest untagged end is used.
*/
$preg =
sprintf("|<!--\s+%s:{0,1}\s+%s(.*?)-->\n*(.*?)<!--\s+end\s+%s:{0,1}\s+%s\s+-->|s",
$Name,
$comp,
$Name,
$comp) ;
if (preg_match($preg, $this->_code, $ret) == 0)
{
$preg =
sprintf("|<!--\s+%s:{0,1}\s+%s(.*?)-->\n*(.*?)<!--\s+end\s+%s\s+-->|s",
$Name,
$comp,
$Name) ;
preg_match($preg, $this->_code, $ret) ;
$return[2] = FALSE ;
}
$return[0]=$ret[2];
$ret[1] = trim($ret[1]) ;
if(!empty($ret[1])){
$args=explode(" ",$ret[1]);
foreach($args as $v){
$v=trim($v);
$v=ereg_replace(quotemeta('"'),'',$v);
list($a,$b)=explode("=",$v);
if(!empty($a))
$return[1][$a]=$b;
}
}
return $return;
}
/**
* Replace an Element of HTML Code
* @param string $Name Identification of element
* @param mixed $args String or Array of values passed in identification
* @param string $el HTML code for substitute the code between the element
* @access private
*/
function _Element_replace($Name, $args=NULL, $el=NULL)
{
if(is_array($args))
{
$comp="\s{1,}";
foreach($args as $key=>$value)
{
$comp.="$key=\"$value\"\s{0,}";
}
}
elseif ($args != NULL)
{
$comp="\s{0,}".$args;
$comp=preg_replace("[\s{1,}]","\s{1,}",$comp);
}
/*
* Allow tagged ends of blocks.
*/
$preg=sprintf("|<!--\s+%s:{0,1}\s+%s(.*?)-->(.*?)<!--\s+end\s+%s:{0,1}\s+%s\s+-->|s",
$Name,
$comp,
$Name,
$comp) ;
if (preg_match($preg, $this->_code) == 0)
{
/*
* If this block doesn't have a tagged end, switch back to the general end
*/
$preg=sprintf("|<!--\s+%s:{0,1}\s+%s(.*?)-->(.*?)<!--\s+end\s+%s\s+-->|s",
$Name,
$comp,
$Name) ;
}
$this->__replace($preg, $el, $this->_code);
}
/**
* Get part of main HTML code
* @param string $Id Identification of element
* @return object Object contains the main code
* @access public
*/
function getCode($Id)
{
$code=$this->_getElement('code', "id=\"$Id\"");
return new IS_Layout($code[0]);
}
/**
* Provide a call point for replacement of HTML code by a
* constant.
* @see IS_Layout::code_replace()
*/
function code_replaceConstant($Id, $Code)
{
$this->code_replace($Id, $Code) ;
}
/**
* Remove part of main HTML code
* @param string $Id Identification of element
* @param string $Code HTML code to replace
* @access public
*/
function code_replace($Id,&$Code)
{
$this->_Element_replace('code', "id=\"$Id\"",$Code);
}
/**
* Remove part of main HTML code
* @param string $Id Identification of element
* @access public
*/
function remove($Id)
{
$this->_Element_replace('remove', "id=\"$Id\"");
}
/**
* Include code to main HTML
* @param object IS_Layout $Inc Object of layout
* @param string [optional] Tag name to be replaced, default is 'include'.
* @access public
*/
function inc(&$Inc, $theTagName='include')
{
$this->replace($theTagName, $Inc->toHtml());
}
/**
* Do a substring replacement of a regular expression with a constant value.
* @see IS_Layout::__replace()
*/
function &__replaceConstant($theRegex, $theValue, &$theString)
{
return $this->__replace($theRegex, $theValue, $theString) ;
}
/**
* Do a substring replacement of a regular expression.
*
* @param string $theRegex the regular expression to be searched for.
* @param string $theValue the value to be inserted.
* @param string $theString [reference] the string to be procesed.
* @return string $theString [reference] the string with the regular
* expression replaced.
* @access private
*/
function &__replace($theRegex, &$theValue, &$theString)
{
if (version_compare('4.3.0', PHP_VERSION, 'le'))
{
/*
* There was a bug using preg_replace directly, especially
* with big strings and replacements. That drove the decision
* to rewrite it this way. Unfortunately PREG_OFFSET_CAPTURE
* isn't available for versions of php prior to 4.3.0 so to
* [most of the time] avoid the bug I hit, the replacement will
* be done both ways.
*/
preg_match_all($theRegex, $theString, $theMatches, PREG_OFFSET_CAPTURE) ;
$theMatches = array_reverse($theMatches[0]) ;
foreach ($theMatches as $theMatch)
{
$theString =
substr($theString, 0, $theMatch[1])
. (string)$theValue
. substr($theString, $theMatch[1] + strlen($theMatch[0])) ;
}
}
else
{
//
// This is an enormously subtle bug possibility. For performance
// reasons, the rest of this code assumes that the replace operation
// is performed on the variable passed in, thus the pass by
// reference. ALL replacements MUST be assigned by value to the
// $theString formal parameter of this routine or things will screw
// up big time.
//
$theString = preg_replace($theRegex, (string)$theValue, $theString) ;
}
return $theString ;
}
/**
* Replace a variable in HTML code
* @param string $Id Identification of element
* @param string $Item code
* @access private
*/
function _doReplace($Id, &$Item)
{
$this->__replace('|' . quotemeta("[$Id]") . '|s', $Item, $this->_code) ;
}
/**
* Replace non used elements
* @access private
*/
function _replace_none()
{
$this->__replaceConstant("|\\[([^\\[]*),([^\\[]*)\\]|",'',$this->_code);
$this->__replaceConstant("|\\[([^\\[]*),([^\\[]*),([^\\[]*)\\]|",'',$this->_code);
$this->__replaceConstant("|<!--\s+code(.*?)-->|s",'',$this->_code);
$this->__replaceConstant("|<!--\s+end\s+code\s+-->|s",'',$this->_code);
$this->_Element_replace('replace');
}
/**
* Return an array containing the tags and the type of tags
* in the current page.
*
* @access public
* @return array $theTags An associative array, keyed by tag, with additional
* typing information.
*/
function &getTags()
{
$theCode = $this->_code ;
$theTags = array() ;
//
// Strip out the loop replacement blocks, the ID is the tag value.
//
preg_match_all('|<!--\s+replace:{0,1}\s+id="(.*?)".*?-->(.*?)<!--\s+end\s+replace\s+-->|s',
$theCode, $theMatchArray) ;
//
// Organize and remove the loop replacement blocks
// from the code body.
//
foreach ($theMatchArray[1] as $theIndex=>$theVariable)
{
$theTags[$theVariable]['type'] = 'loop' ;
$theTags[$theVariable]['body'] = $theMatchArray[2][$theIndex] ;
$theCode = $this->__replaceConstant("|" . quotemeta($theMatchArray[0][$theIndex]) . "|s",
'', $theCode) ;
preg_match_all('|\[([^,\]]+?)\]|s',
$theMatchArray[0][$theIndex],
$theMatchArray2) ;
if (empty($theMatchArray[0]))
{
//
// The loop replacement block didn't have any fields defined
// in it, get rid of it.
//
unset($theTags[$theVariable]) ;
}
else
{
$theTags[$theVariable]['index'] = $theMatchArray2[1] ;
}
}
//
// Strip out the code replacement blocks, the ID is the tag value.
//
preg_match_all('|<!--\s+code:{0,1}\s+id="(.*?)".*?-->(.*?)<!--\s+end\s+code\s+-->|s',
$theCode, $theMatchArray) ;
//
// Organize and remove the code replacement blocks
// from the code body.
//
foreach ($theMatchArray[1] as $theIndex=>$theVariable)
{
$theTags[$theVariable]['type'] = 'code' ;
$theTags[$theVariable]['body'] = $theMatchArray[2][$theIndex] ;
$theCode = $this->__replaceConstant("|" . quotemeta($theMatchArray[0][$theIndex]) . "|s",
'', $theCode) ;
}
//
// Extract all references of the form:
//
// [variable]
// [variable,index]
// [variable,index1,index2,...]
//
preg_match_all('|\[([^,\]]+?(:?,[^,\]]+?){0,})\]|s', $theCode, $theMatchArray) ;
foreach ($theMatchArray[1] as $theReference)
{
$xxx = explode(',', $theReference) ;
$theTags[$xxx[0]]['type'] = (count($xxx) == 1 ? 'scalar' : 'array') ;
if (count($xxx) > 1)
{
$theTags[$xxx[0]]['index'] = array_slice($xxx, 1) ;
}
}
return $theTags ;
}
/**
* Return HTML code
* @return string $Code Code HTML
* @access public
*/
function toHtmlConstant()
{
$this->_replace_none();
return $this->_code;
}
function &toHtml()
{
$this->_replace_none();
return $this->_code;
}
function toRawHtmlConstant()
{
return $this->_code ;
}
/**
* Return HTML code, including the leftover tags.
*
* @access public
* @return string $code Code HTML and leftover tags.
*/
function &toRawHtml()
{
return $this->_code ;
}
/**
* Print HTML code
* @access public
*/
function display()
{
if(!$this->_linesSet)
return False;
echo $this->toHtml();
}
}
?>