<?
/**
* ==[ About SMlite ]==========================================================
* This class is a lightweight template engine. It's based around operating with
* chunks of HTML code and the main aims are:
*
* - completely remove any code from templates
* - make templates editable with HTML editor
*
* @author Romans <hide@address.com>
* @copyright LGPL. See http://www.gnu.org/copyleft/lesser.html
* @version 1.1
* @compat php5 (perhaps php4 untested)
*
* ==[ Version History ]=======================================================
* 1.0 First public version (released with AModules3 alpha)
* 1.1 Added support for "_top" tag
* Removed support for permanent tags
* Much more comments and other fixes
*
* ==[ Description ]===========================================================
* SMlite templates are HTML pages containing tags to mark certain regions.
* <html><head>
* <title>MySite.com - <?page_name?>unknown page<?/page_name?></title>
* </head>
*
* Inside your application regions may be manipulated in a few ways:
*
* - you can replace region with other content. Using this you can replace
* name of sub-page or put a date on your template.
*
* - you can clone whole template or part of it. This is useful if you are
* working with objects
*
* - you can manipulate with regions from different files.
*
* Traditional recipe to work with lists in our templates are:
*
* 1. clone template of generic line
* 2. delete content of the list
* 3. inside loop
* 3a. insert values into cloned template
* 3b. render cloned template
* 3c. insert rendered HTML into list template
* 4. render list template
*
* Inside the code I use terms 'region' and 'spot'. They refer to the same thing,
* but I use 'spot' to refer to a location inside template (such as <?$date?>),
* however I use 'region' when I am refering to a chunk of HTML code or sub-template.
* Sometimes I also use term 'tag' which is like a pointer to region or spot.
*
* When template is loaded it's parsed and converted into array. It's possible to
* cache parsed template serialized inside array.
*
* Tag name looks like this:
*
* "misc/listings:student_list"
*
* Which means to seek tag <?student_list?> inside misc/listings.html
*
* You may have same tag several times inside template. For example you can
* use tag <?$title?> inside <head><title> and <h1>.
*
* If you would set('title','My Title'); it will insert that value in
* all those regions.
*
* ==[ AModules3 integration ]=================================================
* Rule of thumb in object oriented programming is data / code separation. In
* our case HTML is data and our PHP files are code. SMlite helps to completely
* cut out the code from templates (smarty promotes idea about integrating
* logic inside templates and I decided not to use it for that reason)
*
* Inside AModules3, each object have it's own template or may have even several
* templates. When object is created, it's assigned to region inside template.
* Later object operates with assigned template.
*
* Each object is also assigned to a spot on their parent's template. When
* object is rendered, it's HTML is inserted into parent's template.
*
* ==[ Non-AModules3 integration ]=============================================
* SMlite have no strict bindings or requirements for AModules3. You are free
* to use it inside any other library as long as you follow license agreements.
*/
class SMlite extends AbstractModel {
var $tags = array();
/*
* This array contains list of all tags found inside template.
*/
var $top_tag = null;
/*
* When cloning region inside a template, it's tag becomes a top_tag of a new
* template. Since SMlite 1.1 it's present in new template and can be used.
*/
var $template=array(); // private
/*
* This is a parsed contents of the template.
*/
var $settings=array();
/**
* list of updated tags with values
*/
private $updated_tag_list = array();
function getTagVal($tag) {
return (isset($this->updated_tag_list[$tag]))?$this->updated_tag_list[$tag]:null;
}
function getDefaultSettings(){
/*
* This function specifies default settings for SMlite. Use
* 2nd argument for constructor to redefine those settings.
*
* A small note why I decided on .html extension. I want to
* point out that template files are and should be valid HTML
* documents. With .html extension those files will be properly
* rendered inside web browser, properly understood inside text
* editor or will be properly treated with wysiwyg html editors.
*/
return array(
'templates'=>'templates', // directory where templates are located.
// You may specify multiple directories
// by separating them with ':'
'ldelim'=>'<?', // tag delimiter
'rdelim'=>'?>',
'extension'=>'.html', // template file extension
);
}
// Template creation, interface functions
function init(){
$path=array();
if(isset($this->api->skin) && $this->api->skin){
$path[]=$this->api->getConfig('smlite/template_dir','templates').'/'.$this->api->skin;
}
$path[]=$this->api->getConfig('smlite/template_dir','templates');
if(defined('AMODULES3_DIR')){
if(isset($this->api->skin) && $this->api->skin){
$path[]=AMODULES3_DIR.'/templates/'.$this->api->skin;
}
$path[]=AMODULES3_DIR.'/templates/shared';
}
$this->settings=$this->getDefaultSettings();
$this->settings['templates']=join(PATH_SEPARATOR,$path);
$this->settings['extension']=$this->api->getConfig('smlite/extension','.html');
}
function addToPath($dir){
$this->settings['templates'].=PATH_SEPARATOR.$dir;
return $this;
}
function SMlite($template=array(),$settings=array()){
/*
* This method creates new instance of SMlite. The proper way to
* all it is:
*
* $template new SMlite();
*
* As argument you should point a tag for top-level region. You may
* also customize settings by passing them as 2nd argument.
*/
if($template)throw new ObsoleteException("Do not create SMlite directly. Use \$api->add('SMlite'). Alternatively you can use one.");
}
function cloneRegion($tag){
/*
* Sometimes you will want to put branch into different class. This function will create
* new class for you.
*/
if($this->isTopTag($tag)){
return clone $this;
}
if(!$this->is_set($tag)){
$o=$this->owner?" for ".$this->owner->__toString():"";
throw new BaseException("No such tag ($tag) in template$o. Tags are: ".join(',',array_keys($this->tags)));
}
$class_name=get_class($this);
$new=new $class_name();
$new->template=$this->tags[$tag][0];
$new->owner=$this->owner;
$new->top_tag=$tag;
$new->settings=$this->settings;
return $new->rebuildTags();
}
// Misc functions
function dumpTags(){
/*
* This function is used for debug. It will output all tag names inside
* current templates
*/
echo "<pre>";
var_Dump(array_keys($this->tags));
echo "</pre>";
}
// Operation with regions inside template
function get($tag){
/*
* Finds tag and returns contents.
*
* THIS FUNTION IS DANGEROUS!
* - if you want a rendered region, use renderRegion()
* - if you want a sub-template use cloneRegion()
*
* - if you want to copy part of template to other SMlite object,
* do not forget to call rebuildTags() if you plan to refer them.
* Not calling rebuildTags() will render template properly anyway.
*
* If tag is defined multiple times, first region is returned.
*/
if($this->isTopTag($tag))return $this->template;
$v=$this->tags[$tag][0];
if(is_array($v) && count($v)==1)$v=array_shift($v);
return $v;
}
function append($tag,$value,$delim=""){
/*
* This appends static content to region refered by a tag. This function
* is useful when you are adding more rows to a list or table.
*
* If you have specified $delim, it will be used as a separator
* between existing content and newly appended.
*
* If tag is used for several regions inide template, they all will be
* appended with new data.
*/
if($this->isTopTag($tag)){
if ($this->template){
$this->template[]=$delim;
}
$this->template[]=$value;
return $this;
}
if(!isset($this->tags[$tag]) || !is_array($this->tags[$tag])){
$this->fatal("Cannot append to tag $tag");
}
foreach($this->tags[$tag] as $key=>$_){
if ($this->tags[$tag][$key]){
$this->tags[$tag][$key][]=$delim;
}
$this->tags[$tag][$key][]=$value;
}
return $this;
}
function set($tag,$value=null){
/*
* This function will replace region refered by $tag to a new content.
*
* If tag is used several times, all regions are replaced.
*
* ALTERNATIVE USE(2) of this function is to pass associative array as
* a single argument. This will assign multiple tags with one call.
* Sample use is:
*
* set($_GET);
*
* would read and set multiple region values from $_GET array.
*
* ALTERNATIVE USE(3) of this function is to pass 2 arrays. First array
* will contain tag names and 2nd array will contain their values.
*/
if(is_array($tag)){
if(is_null($value)){
// USE(2)
foreach($tag as $s=>$v){
$this->trySet($s,$v);
}
return $this;
}
if(is_array($value)){
// USE(2)
reset($tag);reset($value);
while(list(,$s)=each($tag)){
list(,$v)=each($value);
$this->set($s,$v);
}
return $this;
}
$this->fatal("Incorrect argument types when calling SMlite::set(). Check documentation.");
}
if($this->isTopTag($tag)){
$this->template=$value;
return $this;
}
if(!isset($this->tags[$tag])||!is_array($this->tags[$tag])){
return $this->fatal("No such tag '$tag' inside SMlite::set()");
}
foreach($this->tags[$tag] as $key=>$_){
$this->tags[$tag][$key]=$value;
}
$this->updated_tag_list[$tag] = $value;
return $this;
}
function is_set($tag){
/*
* Check if tag is present inside template
*/
if($this->isTopTag($tag))return true;
return isset($this->tags[$tag]) && is_array($this->tags[$tag]);
}
function trySet($tag,$value=null){
/*
* Check if tag is present inside template. If it does, execute set(); See documentation
* for set()
*/
if(is_array($tag))return $this->set($tag,$value);
return $this->is_set($tag)?$this->set($tag,$value):$this;
}
function del($tag){
/*
* This deletes content of a region, however tag remains and you can still refer to it.
*
* If tag is defined multiple times, content of all regions are deleted.
*/
if($this->isTopTag($tag)){
$this->loadTemplateFromString('<?$'.$tag.'?>');
return $this;
//return $this->fatal("SMlite::del() is trying to delete top tag: $tag");
}
if(empty($this->tags[$tag])){
$o=$this->owner?" for ".$this->owner->__toString():"";
throw new BaseException("No such tag ($tag) in template$o. Tags are: ".join(',',array_keys($this->tags)));
}
foreach($this->tags[$tag] as $key=>$val){
$this->tags[$tag][$key]=array();
}
unset($this->updated_tag_list[$tag]);
return $this;
}
function tryDel($tag){
if(is_array($tag))return $this->del($tag);
return $this->is_set($tag)?$this->del($tag):$this;
}
function eachTag($tag,$callable){
/*
* This function will execute $callable($text) for each
* occurance of $tag. This is handy if one tag appears several times on the page,
* but needs custom processing. $text will be rendered part of the template
*/
if(!isset($this->tags[$tag]))return;
$t=$this->tags[$tag];
foreach($t as $key=>$text){
$this->tags[$tag][$key][0]=$callable($this->renderRegion($text));
}
}
// template loading and parsing
function findTemplate($template_name){
/*
* Find template location inside search directory path
*/
$tmp_locations = split(PATH_SEPARATOR,$this->settings['templates']);
foreach($tmp_locations as $loc)if($loc){
if(file_exists($f=$loc.'/'.$template_name.$this->settings['extension'])){
return join('',file($f));
}
}
return null;
}
function loadTemplateFromString($template_string){
$this->template=array();
$this->tags=array();
$this->updated_tag_list = array();
$this->tmp_template=$template_string;
$this->parseTemplate($this->template);
return $this;
}
function loadTemplate($template_name,$ext=null){
/*
* Load template from file
*/
if($ext){
$tempext=$this->settings['extension'];
$this->settings['extension']=$ext;
};
$this->tmp_template = $this->findTemplate($template_name);
if(!isset($this->tmp_template))
throw new SMliteException("Template not found (".$template_name.$this->settings['extension'].") in (". $this->settings['templates'].")");
$this->parseTemplate($this->template);
if($ext){ $this->settings['extension']=$tempext; }
return $this;
}
function parseTemplate(&$template){
/*
* private function
*
* This is a main function, which actually parses template. It's recursive and it
* calls itself. Empty array should be passed
*/
while(strlen($this->tmp_template)){
$text = $this->myStrTok($this->tmp_template,$this->settings['ldelim']);
if($text)$template[]=$text;
$tag=trim($this->myStrTok($this->tmp_template,$this->settings['rdelim']));
$c=count($template);
if(substr($tag,0,1)=='$'){
$tag = substr($tag,1);
$template[$tag.'#'.$c]=array();
$this->registerTag($tag,$template[$tag.'#'.$c]);
}elseif(substr($tag,0,1)=='/'){
$tag = substr($tag,1);
return $tag;
}elseif(substr($tag,-1,1)=='/'){
$tag = substr($tag,0,-1);
$template[$tag.'#'.$c]=array();
$this->registerTag($tag,$template[$tag.'#'.$c]);
}elseif(isset($tag) && $tag){
$template[$tag.'#'.$c]=array();
$this->registerTag($tag,$template[$tag.'#'.$c]);
$xtag = $this->parseTemplate($template[$tag.'#'.$c]);
if($xtag && $tag!=$xtag){
throw new BaseException("Tag missmatch. $tag is closed with $xtag");
}
}
}
return "end_of_file";
}
function registerTag($key,&$ref){
if(!$key)return;
$this->tags[$key][]=&$ref;
}
function isTopTag($tag){
return
(isset($this->top_tag) && ($tag==$this->top_tag)) ||
($tag=='_top');
}
// rebuild tags of existing array structure
function rebuildTags(){
/*
* This function walks through template and rebuilds list of tags. You need it in case you
* changed already parsed template.
*/
$this->tags=array();
$this->updated_tag_list = array();
$this->rebuildTagsRegion($this->template);
return $this;
}
function rebuildTagsRegion(&$branch){
if(!isset($branch))throw new BaseException("Cannot rebuild tags, because template is empty");
foreach($branch as $key=>$val){
if(is_int($key))continue;
list($real_key,$junk)=split('#',$key);
$this->registerTag($real_key,$branch[$key]);
if(is_array($branch[$key]))$this->rebuildTagsRegion($branch[$key]);
}
}
// Template rendering (array -> string)
function render(){
/*
* This function should be used to convert template into string representation.
*/
return $this->renderRegion($this->template);
}
function renderRegion(&$chunk){
$result = '';
if(!is_array($chunk))return $chunk;
foreach($chunk as $key=>$_chunk){
if(is_array($result)){
$result[]=$this->renderRegion($_chunk);
}else{
$tmp=$this->renderRegion($_chunk);
if(is_array($tmp)){
$result=array($result);
$result[]=$tmp;
}else{
$result.=$tmp;
}
}
}
return $result;
}
// Misc functions
function myStrTok(&$string,$tok){
if(!$string)return '';
$pos = strpos($string,$tok);
if($pos===false){
$chunk=$string;
$string='';
return $chunk; // nothing left
}
$chunk = substr($string,0,$pos);
$string = substr($string,$pos+strlen($tok));
return $chunk;
}
}