PageRenderTime 44ms CodeModel.GetById 12ms RepoModel.GetById 1ms app.codeStats 0ms

/atk4/lib/SMlite.php

https://github.com/mahimarathore/mahi
PHP | 580 lines | 315 code | 29 blank | 236 comment | 64 complexity | 2cd545c18cb722c7edc801b3c8165b82 MD5 | raw file
Possible License(s): AGPL-3.0, MPL-2.0-no-copyleft-exception
  1. <?php // vim:ts=4:sw=4:et:fdm=marker
  2. /*
  3. * Undocumented
  4. *
  5. * @link http://agiletoolkit.org/
  6. *//*
  7. ==ATK4===================================================
  8. This file is part of Agile Toolkit 4
  9. http://agiletoolkit.org/
  10. (c) 2008-2013 Agile Toolkit Limited <info@agiletoolkit.org>
  11. Distributed under Affero General Public License v3 and
  12. commercial license.
  13. See LICENSE or LICENSE_COM for more information
  14. =====================================================ATK4=*/
  15. /**
  16. * ==[ About SMlite ]==========================================================
  17. * This class is a lightweight template engine. It's based around operating with
  18. * chunks of HTML code and the main aims are:
  19. *
  20. * - completely remove any code from templates
  21. * - make templates editable with HTML editor
  22. *
  23. * @author Romans <romans@adevel.com>
  24. * @copyright LGPL. See http://www.gnu.org/copyleft/lesser.html
  25. * @version 1.1
  26. * @compat php5 (perhaps php4 untested)
  27. *
  28. * ==[ Version History ]=======================================================
  29. * 1.0 First public version (released with AModules3 alpha)
  30. * 1.1 Added support for "_top" tag
  31. * Removed support for permanent tags
  32. * Much more comments and other fixes
  33. *
  34. * ==[ Description ]===========================================================
  35. * SMlite templates are HTML pages containing tags to mark certain regions.
  36. * <html><head>
  37. * <title>MySite.com - <?page_name?>unknown page<?/page_name?></title>
  38. * </head>
  39. *
  40. * Inside your application regions may be manipulated in a few ways:
  41. *
  42. * - you can replace region with other content. Using this you can replace
  43. * name of sub-page or put a date on your template.
  44. *
  45. * - you can clone whole template or part of it. This is useful if you are
  46. * working with objects
  47. *
  48. * - you can manipulate with regions from different files.
  49. *
  50. * Traditional recipe to work with lists in our templates are:
  51. *
  52. * 1. clone template of generic line
  53. * 2. delete content of the list
  54. * 3. inside loop
  55. * 3a. insert values into cloned template
  56. * 3b. render cloned template
  57. * 3c. insert rendered HTML into list template
  58. * 4. render list template
  59. *
  60. * Inside the code I use terms 'region' and 'spot'. They refer to the same thing,
  61. * but I use 'spot' to refer to a location inside template (such as <?$date?>),
  62. * however I use 'region' when I am refering to a chunk of HTML code or sub-template.
  63. * Sometimes I also use term 'tag' which is like a pointer to region or spot.
  64. *
  65. * When template is loaded it's parsed and converted into array. It's possible to
  66. * cache parsed template serialized inside array.
  67. *
  68. * Tag name looks like this:
  69. *
  70. * "misc/listings:student_list"
  71. *
  72. * Which means to seek tag <?student_list?> inside misc/listings.html
  73. *
  74. * You may have same tag several times inside template. For example you can
  75. * use tag <?$title?> inside <head><title> and <h1>.
  76. *
  77. * If you would set('title','My Title'); it will insert that value in
  78. * all those regions.
  79. *
  80. * ==[ AModules3 integration ]=================================================
  81. * Rule of thumb in object oriented programming is data / code separation. In
  82. * our case HTML is data and our PHP files are code. SMlite helps to completely
  83. * cut out the code from templates (smarty promotes idea about integrating
  84. * logic inside templates and I decided not to use it for that reason)
  85. *
  86. * Inside AModules3, each object have it's own template or may have even several
  87. * templates. When object is created, it's assigned to region inside template.
  88. * Later object operates with assigned template.
  89. *
  90. * Each object is also assigned to a spot on their parent's template. When
  91. * object is rendered, it's HTML is inserted into parent's template.
  92. *
  93. * ==[ Non-AModules3 integration ]=============================================
  94. * SMlite have no strict bindings or requirements for AModules3. You are free
  95. * to use it inside any other library as long as you follow license agreements.
  96. */
  97. class SMlite extends AbstractModel {
  98. var $tags = array();
  99. /*
  100. * This array contains list of all tags found inside template.
  101. */
  102. var $top_tag = null;
  103. /*
  104. * When cloning region inside a template, it's tag becomes a top_tag of a new
  105. * template. Since SMlite 1.1 it's present in new template and can be used.
  106. */
  107. var $template=array(); // private
  108. /*
  109. * This is a parsed contents of the template.
  110. */
  111. var $settings=array();
  112. /**
  113. * Type of resource to look for pathFinder
  114. */
  115. var $template_type='template';
  116. /**
  117. * list of updated tags with values
  118. */
  119. public $updated_tag_list = array();
  120. private $cache;
  121. function getTagVal($tag) {
  122. return (isset($this->updated_tag_list[$tag]))?$this->updated_tag_list[$tag]:null;
  123. }
  124. function getDefaultSettings(){
  125. /*
  126. * This function specifies default settings for SMlite. Use
  127. * 2nd argument for constructor to redefine those settings.
  128. *
  129. * A small note why I decided on .html extension. I want to
  130. * point out that template files are and should be valid HTML
  131. * documents. With .html extension those files will be properly
  132. * rendered inside web browser, properly understood inside text
  133. * editor or will be properly treated with wysiwyg html editors.
  134. */
  135. return array(
  136. // by separating them with ':'
  137. 'ldelim'=>'<?', // tag delimiter
  138. 'rdelim'=>'?>',
  139. 'extension'=>'.html', // template file extension
  140. );
  141. }
  142. // Template creation, interface functions
  143. function init(){
  144. parent::init();
  145. $path=array();
  146. $this->cache=&$this->api->smlite_cache;
  147. $this->settings=$this->getDefaultSettings();
  148. $this->settings['extension']=$this->api->getConfig('smlite/extension','.html');
  149. }
  150. function __clone(){
  151. if(!is_null($this->top_tag)&&is_object($this->top_tag))$this->top_tag=clone $this->top_tag;
  152. // may be some of the following lines are unneeded...
  153. $this->template=unserialize(serialize($this->template));
  154. $this->tags=unserialize(serialize($this->tags));
  155. $this->settings=unserialize(serialize($this->settings));
  156. $this->updated_tag_list=unserialize(serialize($this->updated_tag_list));
  157. // ...
  158. $this->rebuildTags();
  159. }
  160. function cloneRegion($tag){
  161. /*
  162. * Sometimes you will want to put branch into different class. This function will create
  163. * new class for you.
  164. */
  165. if($this->isTopTag($tag)){
  166. $class_name=get_class($this);
  167. $new=$this->add($class_name);
  168. $new->template=unserialize(serialize($this->template));
  169. $new->top_tag=$tag;
  170. $new->settings=$this->settings;
  171. $new->rebuildTags();
  172. return $new;
  173. }
  174. if(!$this->hasTag($tag)){
  175. $o=$this->owner?" for ".$this->owner->__toString():"";
  176. throw new BaseException("No such tag ($tag) in template$o. Tags are: ".join(', ',array_keys($this->tags)));
  177. }
  178. $class_name=get_class($this);
  179. $new=$this->add($class_name);
  180. try {
  181. $new->template=unserialize(serialize($this->tags[$tag][0]));
  182. if(is_string($new->template))$new->template=array($new->template);
  183. }catch(PDOException $e){
  184. var_Dump($this->tags);
  185. throw $this->exception('PDO got stuck in template')
  186. ->addMoreInfo('tag',$tag);
  187. }
  188. $new->top_tag=$tag;
  189. $new->settings=$this->settings;
  190. return $new->rebuildTags();
  191. }
  192. // Misc functions
  193. function dumpTags(){
  194. /*
  195. * This function is used for debug. It will output all tag names inside
  196. * current templates
  197. */
  198. echo "<pre>";
  199. var_Dump(array_keys($this->tags));
  200. echo "</pre>";
  201. }
  202. // Operation with regions inside template
  203. function get($tag){
  204. /*
  205. * Finds tag and returns contents.
  206. *
  207. * THIS FUNTION IS DANGEROUS!
  208. * - if you want a rendered region, use renderRegion()
  209. * - if you want a sub-template use cloneRegion()
  210. *
  211. * - if you want to copy part of template to other SMlite object,
  212. * do not forget to call rebuildTags() if you plan to refer them.
  213. * Not calling rebuildTags() will render template properly anyway.
  214. *
  215. * If tag is defined multiple times, first region is returned.
  216. */
  217. if($this->isTopTag($tag))return $this->template;
  218. $v=$this->tags[$tag][0];
  219. if(is_array($v) && count($v)==1)$v=array_shift($v);
  220. return $v;
  221. }
  222. function appendHTML($tag,$value){
  223. return $this->append($tag,$value,false);
  224. }
  225. function append($tag,$value,$encode=true){
  226. /*
  227. * This appends static content to region refered by a tag. This function
  228. * is useful when you are adding more rows to a list or table.
  229. *
  230. * If you have specified $delim, it will be used as a separator
  231. * between existing content and newly appended.
  232. *
  233. * If tag is used for several regions inide template, they all will be
  234. * appended with new data.
  235. */
  236. if(!is_bool($encode))throw $this->exception('$delim is depreciated. Now 3rd argument is boolean');
  237. if($value instanceof URL)$value=$value->__toString();
  238. // Temporary here until we finish testing
  239. if($encode && $value!=htmlspecialchars($value,ENT_NOQUOTES,'UTF-8') && $this->api->getConfig('html_injection_debug',false))throw $this->exception('Attempted to supply html string through append()')
  240. ->addMoreInfo('val',var_export($value,true))
  241. ->addMoreInfo('enc',var_export(htmlspecialchars($value,ENT_NOQUOTES,'UTF-8'),true))
  242. //->addAction('ignore','Ignore tag'.$tag)
  243. ;
  244. if($encode)$value=htmlspecialchars($value,ENT_NOQUOTES,'UTF-8');
  245. if($this->isTopTag($tag)){
  246. /*
  247. if ($this->template){
  248. $this->template[]=$delim;
  249. }
  250. */
  251. $this->template[]=$value;
  252. return $this;
  253. }
  254. if(!isset($this->tags[$tag]) || !is_array($this->tags[$tag])){
  255. throw $this->exception("Cannot append to tag $tag")
  256. ->addMoreInfo('by',$this->owner);
  257. }
  258. foreach($this->tags[$tag] as $key=>$_){
  259. if(!is_array($this->tags[$tag][$key])){
  260. //throw new BaseException("Problem appending '".htmlspecialchars($value)."' to '$tag': key=$key");
  261. $this->tags[$tag][$key]=array($this->tags[$tag][$key]);
  262. }
  263. /*
  264. if ($this->tags[$tag][$key]){
  265. $this->tags[$tag][$key][]=$delim;
  266. }
  267. */
  268. $this->tags[$tag][$key][]=$value;
  269. }
  270. return $this;
  271. }
  272. function setHTML($tag,$value=null){
  273. return $this->set($tag,$value,false);
  274. }
  275. function set($tag,$value=null,$encode=true){
  276. /*
  277. * This function will replace region refered by $tag to a new content.
  278. *
  279. * If tag is used several times, all regions are replaced.
  280. *
  281. * ALTERNATIVE USE(2) of this function is to pass associative array as
  282. * a single argument. This will assign multiple tags with one call.
  283. * Sample use is:
  284. *
  285. * set($_GET);
  286. *
  287. * would read and set multiple region values from $_GET array.
  288. *
  289. * ALTERNATIVE USE(3) of this function is to pass 2 arrays. First array
  290. * will contain tag names and 2nd array will contain their values.
  291. */
  292. if(is_object($tag))$tag=$tag->get();
  293. if(is_array($tag)){
  294. if(is_null($value)){
  295. // USE(2)
  296. foreach($tag as $s=>$v){
  297. $this->trySet($s,$v,$encode);
  298. }
  299. return $this;
  300. }
  301. if(is_array($value)){
  302. // USE(2)
  303. reset($tag);reset($value);
  304. while(list(,$s)=each($tag)){
  305. list(,$v)=each($value);
  306. $this->set($s,$v,$encode);
  307. }
  308. return $this;
  309. }
  310. $this->fatal("Incorrect argument types when calling SMlite::set(). Check documentation.");
  311. }
  312. if($value instanceof URL)$value=$value->__toString();
  313. if(is_array($value))return $this;
  314. if($encode && $value!=htmlspecialchars($value,ENT_NOQUOTES,'UTF-8') && $this->api->getConfig('html_injection_debug',false))throw $this->exception('Attempted to supply html string through set()')
  315. ->addMoreInfo('val',var_export($value,true))
  316. ->addMoreInfo('enc',var_export(htmlspecialchars($value,ENT_NOQUOTES,'UTF-8'),true))
  317. //->addAction('ignore','Ignore tag'.$tag)
  318. ;
  319. if($encode)$value=htmlspecialchars($value,ENT_NOQUOTES,'UTF-8');
  320. if($this->isTopTag($tag)){
  321. $this->template=$value;
  322. return $this;
  323. }
  324. if(!isset($this->tags[$tag])||!is_array($this->tags[$tag])){
  325. $o=$this->owner?$this->owner->__toString():"none";
  326. throw $this->exception("No such tag in template")
  327. ->addMoreInfo('tag',$tag)
  328. ->addMoreInfo('owner',$o)
  329. ->addMoreInfo('tags',join(', ',array_keys($this->tags)));
  330. }
  331. foreach($this->tags[$tag] as $key=>$_){
  332. $this->tags[$tag][$key]=$value;
  333. }
  334. $this->updated_tag_list[$tag] = $value;
  335. return $this;
  336. }
  337. /** Check if tag is present inside template */
  338. function hasTag($tag){
  339. if($this->isTopTag($tag))return true;
  340. return isset($this->tags[$tag]) && is_array($this->tags[$tag]);
  341. }
  342. function is_set($tag){
  343. return $this->hasTag($tag);
  344. }
  345. function trySetHTML($tag,$value=null){
  346. return $this->trySet($tag,$value,false);
  347. }
  348. function trySet($tag,$value=null,$encode=true){
  349. /*
  350. * Check if tag is present inside template. If it does, execute set(); See documentation
  351. * for set()
  352. */
  353. if(is_array($tag))return $this->set($tag,$value,$encode);
  354. return $this->hasTag($tag)?$this->set($tag,$value,$encode):$this;
  355. }
  356. function del($tag){
  357. /*
  358. * This deletes content of a region, however tag remains and you can still refer to it.
  359. *
  360. * If tag is defined multiple times, content of all regions are deleted.
  361. */
  362. if($this->isTopTag($tag)){
  363. $this->loadTemplateFromString('<?$'.$tag.'?>');
  364. return $this;
  365. //return $this->fatal("SMlite::del() is trying to delete top tag: $tag");
  366. }
  367. if(empty($this->tags[$tag])){
  368. $o=$this->owner?" for ".$this->owner->__toString():"";
  369. throw new BaseException("No such tag ($tag) in template$o. Tags are: ".join(', ',array_keys($this->tags)));
  370. }
  371. foreach($this->tags[$tag] as $key=>$val){
  372. $this->tags[$tag][$key]=array();
  373. }
  374. unset($this->updated_tag_list[$tag]);
  375. return $this;
  376. }
  377. function tryDel($tag){
  378. if(is_array($tag))return $this->del($tag);
  379. return $this->hasTag($tag)?$this->del($tag):$this;
  380. }
  381. function eachTag($tag,$callable){
  382. /*
  383. * This function will execute $callable($text,$tag) for each
  384. * occurance of $tag. This is handy if one tag appears several times on the page,
  385. * but needs custom processing. $text will be rendered part of the template. $tag
  386. * will be unique reference to a tag, containing #<num> allowing you to add objects
  387. * from the functions
  388. */
  389. if(!isset($this->tags[$tag]))return;
  390. foreach($this->tags as $tagx=>$arr){
  391. $tag_split=explode('#',$tagx);
  392. $t=$tag_split[0];
  393. if(!isset($tag_split[1]) || $t!=$tag)continue;
  394. $text=$this->tags[$tagx][0][0];
  395. $ret=call_user_func($callable,$this->renderRegion($text),$tagx);
  396. if($ret instanceof URL)$ret=$ret->__toString();
  397. $x=$this->tags[$tagx][0][0]=$ret;
  398. }
  399. }
  400. // template loading and parsing
  401. function findTemplate($template_name){
  402. /*
  403. * Find template location inside search directory path
  404. */
  405. if(!$this->api)throw new Exception_InitError('You should use add() to add objects!');
  406. $f=$this->api->locatePath($this->template_type,$template_name.$this->settings['extension']);
  407. return join('',file($f));
  408. }
  409. function loadTemplateFromString($template_string){
  410. $this->template=array();
  411. $this->tags=array();
  412. $this->updated_tag_list = array();
  413. $this->tmp_template=$template_string;
  414. $this->parseTemplate($this->template);
  415. return $this;
  416. }
  417. function loadTemplate($template_name,$ext=null){
  418. /*
  419. * Load template from file
  420. */
  421. if(!$this->api)throw new Exception('Broken Link');
  422. if($this->cache[$template_name.$ext]){
  423. $this->template=unserialize($this->cache[$template_name.$ext]);
  424. $this->rebuildTags();
  425. return $this;
  426. }
  427. if($ext){
  428. $tempext=$this->settings['extension'];
  429. $this->settings['extension']=$ext;
  430. };
  431. $this->tmp_template = $this->findTemplate($template_name);
  432. if(!isset($this->tmp_template))
  433. throw new SMliteException("Template not found (".$template_name.$this->settings['extension'].") in (". $this->settings['templates'].")");
  434. $this->parseTemplate($this->template);
  435. if($ext){ $this->settings['extension']=$tempext; }
  436. $this->cache[$template_name.$ext]=serialize($this->template);
  437. return $this;
  438. }
  439. function parseTemplate(&$template,$level=0,$pc=0){
  440. /*
  441. * private function
  442. *
  443. * This is a main function, which actually parses template. It's recursive and it
  444. * calls itself. Empty array should be passed
  445. */
  446. // TODO when we go into sublevel, we should set the number of
  447. // the tag so that there is NO double numbers in template COMPLETELY
  448. // May be this way is dirty, need to look for better solution...
  449. $c=pow(10,$level)+$pc;
  450. while(strlen($this->tmp_template)){
  451. $text = $this->myStrTok($this->tmp_template,$this->settings['ldelim']);
  452. if($text!=='')$template[]=$text;
  453. $tag=trim($this->myStrTok($this->tmp_template,$this->settings['rdelim']));
  454. if(isset($tag)&&$tag){
  455. if($tag[0]=='$'){
  456. $tag = substr($tag,1);
  457. $template[$tag.'#'.$c]=array();
  458. $this->registerTag($tag,$c,$template[$tag.'#'.$c]);
  459. }elseif($tag[0]=='/'){
  460. $tag = substr($tag,1);
  461. return $tag;
  462. }elseif(substr($tag,-1)=='/'){
  463. $tag = substr($tag,0,-1);
  464. $template[$tag.'#'.$c]=array();
  465. $this->registerTag($tag,$c,$template[$tag.'#'.$c]);
  466. }else{
  467. $template[$tag.'#'.$c]=array();
  468. $this->registerTag($tag,$c,$template[$tag.'#'.$c]);
  469. $xtag = $this->parseTemplate($template[$tag.'#'.$c],$level+1,$c);
  470. if($xtag && $tag!=$xtag){
  471. throw new BaseException("Tag missmatch. $tag is closed with $xtag");
  472. }
  473. }
  474. }
  475. $c++;
  476. }
  477. return "end_of_file";
  478. }
  479. function registerTag($key,$npk,&$ref){
  480. if(!$key)return;
  481. if(isset($npk))$this->tags[$key.'#'.$npk][]=&$ref;
  482. $this->tags[$key][]=&$ref;
  483. }
  484. function isTopTag($tag){
  485. return
  486. (isset($this->top_tag) && ($tag==$this->top_tag)) ||
  487. ($tag=='_top');
  488. }
  489. // rebuild tags of existing array structure
  490. function rebuildTags(){
  491. /*
  492. * This function walks through template and rebuilds list of tags. You need it in case you
  493. * changed already parsed template.
  494. */
  495. $this->tags=array();
  496. $this->updated_tag_list = array();
  497. $this->rebuildTagsRegion($this->template);
  498. return $this;
  499. }
  500. function rebuildTagsRegion(&$branch){
  501. if(!isset($branch))throw new BaseException("Cannot rebuild tags, because template is empty");
  502. if(!is_array($branch))throw $this->exception('System problem with SMLite. Incorrect use of branch');
  503. foreach($branch as $key=>$val){
  504. if(is_int($key))continue;
  505. list($real_key,$junk)=explode('#',$key);
  506. $this->registerTag($real_key,null,$branch[$key]);
  507. $this->registerTag($key,null,$branch[$key]);
  508. if(is_array($branch[$key]))$this->rebuildTagsRegion($branch[$key]);
  509. }
  510. }
  511. // Template rendering (array -> string)
  512. function render(){
  513. /*
  514. * This function should be used to convert template into string representation.
  515. */
  516. return $this->renderRegion($this->template);
  517. }
  518. function renderRegion(&$chunk){
  519. $result = '';
  520. if(!is_array($chunk))return $chunk;
  521. foreach($chunk as $key=>$_chunk){
  522. if(is_array($result)){
  523. $result[]=$this->renderRegion($_chunk);
  524. }else{
  525. $tmp=$this->renderRegion($_chunk);
  526. if(is_array($tmp)){
  527. $result=array($result);
  528. $result[]=$tmp;
  529. }else{
  530. $result.=$tmp;
  531. }
  532. }
  533. }
  534. return $result;
  535. }
  536. // Misc functions
  537. function myStrTok(&$string,$tok){
  538. if(!$string)return '';
  539. $pos = strpos($string,$tok);
  540. if($pos===false){
  541. $chunk=$string;
  542. $string='';
  543. return $chunk; // nothing left
  544. }
  545. $chunk = substr($string,0,$pos);
  546. $string = substr($string,$pos+strlen($tok));
  547. return $chunk;
  548. }
  549. }