PageRenderTime 30ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/www/htdocs/include/Spoon/template/template.php

https://github.com/ss23/ECommerce
PHP | 1662 lines | 802 code | 257 blank | 603 comment | 102 complexity | b705ad61c3d37c1536c0495f2a66ce77 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Spoon Library
  4. *
  5. * This source file is part of the Spoon Library. More information,
  6. * documentation and tutorials can be found @ http://www.spoon-library.be
  7. *
  8. * @package template
  9. *
  10. *
  11. * @author Davy Hellemans <davy@spoon-library.be>
  12. * @author Tijs Verkoyen <tijs@spoon-library.be>
  13. * @author Dave Lens <dave@spoon-library.be>
  14. * @since 0.1.1
  15. */
  16. /**
  17. * This exception is used to handle template related exceptions.
  18. *
  19. * @package template
  20. *
  21. *
  22. * @author Davy Hellemans <davy@spoon-library.be>
  23. * @since 0.1.1
  24. */
  25. class SpoonTemplateException extends SpoonException {}
  26. /** Filesystem package */
  27. require_once 'spoon/filesystem/filesystem.php';
  28. /** SpoonDate class */
  29. require_once 'spoon/date/date.php';
  30. /**
  31. * Spoon Library
  32. *
  33. * This source file is part of the Spoon Library. More information,
  34. * documentation and tutorials can be found @ http://www.spoon-library.be
  35. *
  36. * @package template
  37. *
  38. *
  39. * @author Davy Hellemans <davy@spoon-library.be>
  40. * @author Tijs Verkoyen <tijs@spoon-library.be>
  41. * @since 0.1.1
  42. */
  43. class SpoonTemplate
  44. {
  45. /**
  46. * Cache names
  47. *
  48. * @var array
  49. */
  50. private $cache = array();
  51. /**
  52. * Cache directory location
  53. *
  54. * @var string
  55. */
  56. private $cacheDirectory = '.';
  57. /**
  58. * Compile directory location
  59. *
  60. * @var string
  61. */
  62. private $compileDirectory = '.';
  63. /**
  64. * Always recompile
  65. *
  66. * @var bool
  67. */
  68. private $forceCompile = false;
  69. /**
  70. * List of form objects
  71. *
  72. * @var array
  73. */
  74. private $forms = array();
  75. /**
  76. * Stack of variables & their replace values
  77. *
  78. * @var array
  79. */
  80. private $variables = array('CRLF' => "\n", 'TAB' => "\t");
  81. /**
  82. * Adds a form to this template.
  83. *
  84. * @return void
  85. * @param SpoonForm $form
  86. */
  87. public function addForm(SpoonForm $form)
  88. {
  89. $this->forms[$form->getName()] = $form;
  90. }
  91. /**
  92. * Assign values to variables.
  93. *
  94. * @return void
  95. * @param mixed $variable The key to search for or an array with keys & values.
  96. * @param mixed[optional] $value The value to replace the key with. If the first element is an array, this argument is not required.
  97. */
  98. public function assign($variable, $value = null)
  99. {
  100. // regular function use
  101. if($value !== null && $variable != '') $this->variables[(string) $variable] = $value;
  102. // only 1 argument
  103. else
  104. {
  105. // not an array
  106. if(!is_array($variable)) throw new SpoonTemplateException('If you provide one argument it needs to be an array');
  107. // loop array
  108. foreach($variable as $key => $value)
  109. {
  110. // key is NOT empty
  111. if($key !== '') $this->variables[(string) $key] = $value;
  112. }
  113. }
  114. }
  115. /**
  116. * Assign an entire array with keys & values.
  117. *
  118. * @return void
  119. * @param array $values This array with keys and values will be used to search and replace in the template file.
  120. * @param string[optional] $prefix An optional prefix eg. 'lbl' that can be used.
  121. * @param string[optional] $suffix An optional suffix eg. 'msg' that can be used
  122. */
  123. public function assignArray(array $values, $prefix = null, $suffix = null)
  124. {
  125. foreach($values as $key => $value)
  126. {
  127. $this->variables[(string) $prefix . $key . (string) $suffix] = $value;
  128. }
  129. }
  130. /**
  131. * Cache a certain block.
  132. *
  133. * @return void
  134. * @param string $name The name of the block that you want to cache.
  135. * @param int[optional] $lifetime The lifetime in seconds.
  136. */
  137. public function cache($name, $lifetime = 60)
  138. {
  139. // redefine lifetime
  140. $lifetime = (SpoonFilter::isBetween(10, 30758400, $lifetime)) ? (int) $lifetime : 60;
  141. // set lifetime
  142. $this->cache[(string) $name] = $lifetime;
  143. }
  144. /**
  145. * Clear the entire cache or a specific item.
  146. *
  147. * @return void
  148. * @param string[optional] $name The name of the cache block that you want to clear from the directory with cached files.
  149. */
  150. public function clearCache($name = null)
  151. {
  152. // specific cache
  153. if($name !== null) SpoonFile::delete($this->cacheDirectory .'/'. (string) $name .'_cache.tpl');
  154. // all cache files
  155. else
  156. {
  157. // list of *_cache.tpl files from cacheDirectory
  158. $files = SpoonFile::getList($this->cacheDirectory, '|.*\_cache\.tpl|');
  159. // delete
  160. foreach($files as $file) SpoonFile::delete($this->cacheDirectory .'/'. $file);
  161. }
  162. }
  163. /**
  164. * Clear the entire compiled directory or a specific template.
  165. *
  166. * @return void
  167. * @param string[optional] $template The filename of a specific template to mark for recompiling.
  168. */
  169. public function clearCompiled($template = null)
  170. {
  171. // specific template
  172. if($template !== null) SpoonFile::delete($this->compileDirectory .'/'. $this->getCompileName($template));
  173. // all compiled templates
  174. else
  175. {
  176. // list of *.tpl.php files from compileDirectory
  177. $files = SpoonFile::getList($this->compileDirectory, '|.*\.tpl\.php|');
  178. // delete
  179. foreach($files as $file) SpoonFile::delete($this->compileDirectory .'/'. $file);
  180. }
  181. }
  182. /**
  183. * Compile a given template.
  184. *
  185. * @return void
  186. * @param string $path The path to the template, excluding the template filename.
  187. * @param string $template The filename of the template within the path.
  188. */
  189. public function compile($path, $template)
  190. {
  191. // redefine template
  192. if(mb_substr($template, 0, 1, SPOON_CHARSET) != '/') $template = $path .'/'. $template;
  193. // create object
  194. $compiler = new SpoonTemplateCompiler($template, $this->variables);
  195. // set some options
  196. $compiler->setCacheDirectory($this->cacheDirectory);
  197. $compiler->setCompileDirectory($this->compileDirectory);
  198. $compiler->setForceCompile($this->forceCompile);
  199. $compiler->setForms($this->forms);
  200. // compile & save
  201. $compiler->parseToFile();
  202. }
  203. /**
  204. * Returns the correct element from the list, based on the counter.
  205. *
  206. * @return string The item based on the counter from $elements.
  207. * @param int $counter The index of the item to retrieve from the list $elements.
  208. * @param array $elements The list of elements to cycle through.
  209. */
  210. public function cycle($counter, array $elements)
  211. {
  212. // update counter
  213. $counter += 1;
  214. // number of elements
  215. $numElements = count($elements);
  216. // calculate modulus
  217. $modulus = $counter % $numElements;
  218. // leftovers?
  219. if($modulus == 0) return $elements[$numElements - 1];
  220. else return $elements[$modulus - 1];
  221. }
  222. /**
  223. * Deassign a variable.
  224. *
  225. * @return void
  226. * @param string $name The name of the key that you want to remove from the list of already assigned variables.
  227. */
  228. public function deAssign($name)
  229. {
  230. if(isset($this->variables[(string) $name])) unset($this->variables[(string) $name]);
  231. }
  232. /**
  233. * Display the output.
  234. *
  235. * @return void
  236. * @param string $template The filename of the template that you want to display.
  237. */
  238. public function display($template)
  239. {
  240. // validate name
  241. if(trim($template) == '') throw new SpoonTemplateException('Please provide a template.');
  242. // compiled name
  243. $compileName = $this->getCompileName((string) $template);
  244. // compiled if needed
  245. if(!SpoonFile::exists($this->compileDirectory .'/'. $compileName) || $this->forceCompile)
  246. {
  247. // create compiler
  248. $compiler = new SpoonTemplateCompiler((string) $template, $this->variables);
  249. // set some options
  250. $compiler->setCacheDirectory($this->cacheDirectory);
  251. $compiler->setCompileDirectory($this->compileDirectory);
  252. $compiler->setForceCompile($this->forceCompile);
  253. $compiler->setForms($this->forms);
  254. // compile & save
  255. $compiler->parseToFile();
  256. }
  257. // load template
  258. require $this->compileDirectory .'/'. $compileName;
  259. }
  260. /**
  261. * Retrieves the already assigned value.
  262. *
  263. * @return mixed Returns an array, string, int or null
  264. * @param string $variable The name of the variable that you want to retrieve the already assigned value from.
  265. */
  266. public function getAssignedValue($variable)
  267. {
  268. if(isset($this->variables[(string) $variable])) return $this->variables[(string) $variable];
  269. return null;
  270. }
  271. /**
  272. * Get the cache directory path.
  273. *
  274. * @return string The location of the cache directory.
  275. */
  276. public function getCacheDirectory()
  277. {
  278. return $this->cacheDirectory;
  279. }
  280. /**
  281. * Get the compile directory path.
  282. *
  283. * @return string The location of the compile directory.
  284. */
  285. public function getCompileDirectory()
  286. {
  287. return $this->compileDirectory;
  288. }
  289. /**
  290. * Retrieve the compiled name for this template.
  291. *
  292. * @return string The special unique name, used for storing this file once compiled in the compile directory.
  293. * @param string $template The filename of the template.
  294. * @param string[optional] $path The optional path to this template.
  295. */
  296. private function getCompileName($template, $path = null)
  297. {
  298. // redefine template
  299. if(mb_substr($template, 0, 1, SPOON_CHARSET) != '/' && $path !== null) $template = $path .'/'. $template;
  300. // return the correct full path
  301. return md5(realpath($template)) .'_'. basename($template) .'.php';
  302. }
  303. /**
  304. * Fetch the parsed content from this template.
  305. *
  306. * @return string The actual parsed content after executing this template.
  307. * @param string $template The location of the template file, used to display this template.
  308. */
  309. public function getContent($template)
  310. {
  311. // cache tags can not be combined with this method
  312. if(!empty($this->cache)) throw new SpoonTemplateException('You can not use this method when the template uses cache tags.');
  313. // all is fine
  314. // turn on output buffering
  315. ob_start();
  316. // show output
  317. $this->display($template);
  318. // return template content
  319. return ob_get_clean();
  320. }
  321. /**
  322. * Get the force compiling directive.
  323. *
  324. * @return bool Do we need to recompile this template every time it's loaded.
  325. */
  326. public function getForceCompile()
  327. {
  328. return $this->forceCompile;
  329. }
  330. /**
  331. * Get the template language.
  332. *
  333. * @return string The language that's being used as default for this template. (Check SpoonLocale for the list of available languages.)
  334. */
  335. public function getLanguage()
  336. {
  337. return $this->language;
  338. }
  339. /**
  340. * Is the cache for this item still valid.
  341. *
  342. * @return bool Is this template block cached?
  343. * @param string $name The name of the cached block.
  344. */
  345. public function isCached($name)
  346. {
  347. // doesnt exist
  348. if(!isset($this->cache[(string) $name])) throw new SpoonTemplateException('No cache with the name "'. (string) $name .'" is known.');
  349. // last modification date
  350. $time = @filemtime($this->cacheDirectory .'/'. (string) $name .'_cache.tpl');
  351. // doesn't exist
  352. if($time === false) return false;
  353. // not valid
  354. if((time() - (int) $time) > $this->cache[(string) $name]) return false;
  355. // still valid
  356. return true;
  357. }
  358. /**
  359. * Map a modifier to a given function/method.
  360. *
  361. * @return void
  362. * @param string $name The name that you wish to use in the templates as a modifier.
  363. * @param mixed $function The function or method to map this name to. In case it's a method, provide this as an array containing class and method name.
  364. */
  365. public function mapModifier($name, $function)
  366. {
  367. SpoonTemplateModifiers::mapModifier($name, $function);
  368. }
  369. /**
  370. * Set the cache directory.
  371. *
  372. * @return void
  373. * @param string $path The location of the directory where you want to store your cached template blocks.
  374. */
  375. public function setCacheDirectory($path)
  376. {
  377. $this->cacheDirectory = (string) $path;
  378. }
  379. /**
  380. * Set the compile directory.
  381. *
  382. * @return void
  383. * @param string $path The location of the directory where you want to store your compiled templates.
  384. */
  385. public function setCompileDirectory($path)
  386. {
  387. $this->compileDirectory = (string) $path;
  388. }
  389. /**
  390. * If enabled, recompiles a template even if it has already been compiled.
  391. *
  392. * @return void
  393. * @param bool[optional] $on Do we need to recompile the template every time it loads.
  394. */
  395. public function setForceCompile($on = true)
  396. {
  397. $this->forceCompile = (bool) $on;
  398. }
  399. }
  400. /**
  401. * Spoon Library
  402. *
  403. * This source file is part of the Spoon Library. More information,
  404. * documentation and tutorials can be found @ http://www.spoon-library.be
  405. *
  406. * @package template
  407. *
  408. *
  409. * @author Davy Hellemans <davy@spoon-library.be>
  410. * @since 1.0.0
  411. */
  412. class SpoonTemplateCompiler
  413. {
  414. /**
  415. * Cache directory location
  416. *
  417. * @var string
  418. */
  419. private $cacheDirectory = '.';
  420. /**
  421. * Compile directory location
  422. *
  423. * @var string
  424. */
  425. private $compileDirectory = '.';
  426. /**
  427. * Working content
  428. *
  429. * @var string
  430. */
  431. private $content;
  432. /**
  433. * Always recompile
  434. *
  435. * @var bool
  436. */
  437. private $foreCompile = false;
  438. /**
  439. * List of form objects
  440. *
  441. * @var array
  442. */
  443. private $forms = array();
  444. /**
  445. * Cached list of the modifiers
  446. *
  447. * @var array
  448. */
  449. private $modifiers = array();
  450. /**
  451. * Is the content already parsed
  452. *
  453. * @var bool
  454. */
  455. private $parsed = false;
  456. /**
  457. * Template file
  458. *
  459. * @var string
  460. */
  461. private $template;
  462. /**
  463. * List of variables
  464. *
  465. * @var array
  466. */
  467. private $variables = array();
  468. /**
  469. * Class constructor.
  470. *
  471. * @return void
  472. * @param string $template The name of the template to compile.
  473. * @param array $variables The list of possible variables.
  474. */
  475. public function __construct($template, array $variables)
  476. {
  477. $this->template = (string) $template;
  478. $this->variables = $variables;
  479. }
  480. /**
  481. * Retrieve the compiled name for this template.
  482. *
  483. * @return string The unique filename used to store the compiled template in the compile directory.
  484. * @param string $template The name of the template.
  485. */
  486. private function getCompileName($template)
  487. {
  488. return md5(realpath($template)) .'_'. basename($template) .'.php';
  489. }
  490. /**
  491. * Retrieve the content.
  492. *
  493. * @return string The php compiled template.
  494. */
  495. public function getContent()
  496. {
  497. if(!$this->parsed) $this->parse();
  498. return $this->content;
  499. }
  500. /**
  501. * Creates a string of the provided value with the variables encapsulated.
  502. *
  503. * @return string The variable value as php code.
  504. * @param string $value The value that needs to be compiled to php code.
  505. */
  506. private function getVariableString($value)
  507. {
  508. // init var
  509. $variables = array();
  510. // regex
  511. $pattern = '/\{\$([a-z0-9_])+(\.([a-z0-9_])+)?\}/i';
  512. // find variables
  513. if(preg_match_all($pattern, $value, $matches))
  514. {
  515. // loop variables
  516. foreach($matches[0] as $match)
  517. {
  518. $variables[] = $this->parseVariable($match);
  519. }
  520. }
  521. // replace the variables by %s
  522. $value = preg_replace($pattern, '%s', $value);
  523. // encapsulate the vars
  524. $value = "'". str_replace('%s', "'. %s .'", $value) ."'";
  525. // fix errors
  526. if(mb_substr($value, 0, 4, SPOON_CHARSET) == "''. ") $value = mb_substr($value, 4, mb_strlen($value, SPOON_CHARSET), SPOON_CHARSET);
  527. if(mb_substr($value, -4, mb_strlen($value, SPOON_CHARSET), SPOON_CHARSET) == " .''") $value = mb_substr($value, 0, -4, SPOON_CHARSET);
  528. // cleanup
  529. $value = str_replace(".''.", '.', $value);
  530. // add the variables
  531. return vsprintf($value, $variables);
  532. }
  533. /**
  534. * Check the string for syntax errors
  535. *
  536. * @return bool
  537. * @param string $string
  538. * @param string $type
  539. */
  540. private function isCorrectSyntax($string, $type)
  541. {
  542. // init vars
  543. $string = (string) $string;
  544. $type = SpoonFilter::getValue($type, array('cycle', 'iteration', 'option', 'variable'), 'variable', 'string');
  545. // types
  546. switch($type)
  547. {
  548. // cycle string
  549. case 'cycle':
  550. // the number of single qoutes should always be an even number
  551. if(!SpoonFilter::isEven(substr_count($string, "'"))) return false;
  552. break;
  553. // iteration string
  554. case 'iteration':
  555. // the number of square opening/closing brackets should be equal
  556. if(substr_count($string, '[') != substr_count($string, ']')) return false;
  557. // the number of single qoutes should always be an even number
  558. if(!SpoonFilter::isEven(substr_count($string, "'"))) return false;
  559. // first charachter should not be a number
  560. if(SpoonFilter::isInteger(substr($string, 2, 1))) return false;
  561. // square bracket followed by a dot is NOT allowed eg {option:variable[0].var}
  562. if(substr_count($string, '].') != 0) return false;
  563. // dot followed by a square bracket is NOT allowed eg {option:variable.['test']}
  564. if(substr_count($string, '.[') != 0) return false;
  565. // empty brackets are NOT allowed
  566. if(substr_count($string, '[]') != 0) return false;
  567. // empty inversed brackets are NOT allowed
  568. if(substr_count($string, '][') != 0) return false;
  569. break;
  570. // option string
  571. case 'option':
  572. // the number of square opening/closing brackets should be equal
  573. if(substr_count($string, '[') != substr_count($string, ']')) return false;
  574. // the number of single qoutes should always be an even number
  575. if(!SpoonFilter::isEven(substr_count($string, "'"))) return false;
  576. // first charachter should not be a number
  577. if(SpoonFilter::isInteger(substr($string, 2, 1))) return false;
  578. // square bracket followed by a dot is NOT allowed eg {option:variable[0].var}
  579. if(substr_count($string, '].') != 0) return false;
  580. // dot followed by a square bracket is NOT allowed eg {option:variable.['test']}
  581. if(substr_count($string, '.[') != 0) return false;
  582. // empty brackets are NOT allowed
  583. if(substr_count($string, '[]') != 0) return false;
  584. // empty inversed brackets are NOT allowed
  585. if(substr_count($string, '][') != 0) return false;
  586. break;
  587. // variable string
  588. case 'variable':
  589. // the number of square opening/closing brackets should be equal
  590. if(substr_count($string, '[') != substr_count($string, ']')) return false;
  591. // the number of single qoutes should always be an even number
  592. if(!SpoonFilter::isEven(substr_count($string, "'"))) return false;
  593. // first charachter should not be a number
  594. if(SpoonFilter::isInteger(substr($string, 2, 1))) return false;
  595. // square bracket followed by a dot is NOT allowed eg {$variable[0].var}
  596. if(substr_count($string, '].') != 0) return false;
  597. // dot followed by a square bracket is NOT allowed eg {$variable.['test']}
  598. if(substr_count($string, '.[') != 0) return false;
  599. // empty brackets are NOT allowed
  600. if(substr_count($string, '[]') != 0) return false;
  601. // empty inversed brackets are NOT allowed
  602. if(substr_count($string, '][') != 0) return false;
  603. break;
  604. }
  605. return true;
  606. }
  607. /**
  608. * Parse the template.
  609. *
  610. * @return void
  611. */
  612. private function parse()
  613. {
  614. // not yet parsed
  615. if(!$this->parsed)
  616. {
  617. // add to the list of parsed files
  618. $this->files[] = $this->getCompileName($this->template);
  619. // map modifiers
  620. $this->modifiers = SpoonTemplateModifiers::getModifiers();
  621. // set content
  622. $this->content = SpoonFile::getContent($this->template);
  623. // strip php code
  624. $this->content = $this->stripCode($this->content);
  625. // strip comments
  626. $this->content = $this->stripComments($this->content);
  627. // parse iterations
  628. $this->content = $this->parseIterations($this->content);
  629. // includes
  630. $this->content = $this->parseIncludes($this->content);
  631. // parse options
  632. $this->content = $this->parseOptions($this->content);
  633. // parse cache tags
  634. $this->content = $this->parseCache($this->content);
  635. // parse variables
  636. $this->content = $this->parseVariables($this->content);
  637. // parse forms
  638. $this->content = $this->parseForms($this->content);
  639. // while developing, you might want to know about the undefined indexes
  640. $errorReporting = (SPOON_DEBUG) ? 'E_ALL | E_STRICT' : 'E_WARNING';
  641. $displayErrors = (SPOON_DEBUG) ? 'On' : 'Off';
  642. // add error_reporting setting
  643. $this->content = '<?php error_reporting('. $errorReporting .'); ini_set(\'display_errors\', \''. $displayErrors .'\'); ?>'. "\n". $this->content;
  644. // parsed
  645. $this->parsed = true;
  646. }
  647. }
  648. /**
  649. * Parse the cache tags.
  650. *
  651. * @return string The updated content, containing the parsed cache tags.
  652. * @param string $content The content that may contain the parse tags.
  653. */
  654. private function parseCache($content)
  655. {
  656. // regex pattern
  657. $pattern = "/\{cache:([a-z0-9_\.\{\$\}]+)\}.*?\{\/cache:\\1\}/is";
  658. // find matches
  659. if(preg_match_all($pattern, $content, $matches))
  660. {
  661. // loop matches
  662. foreach($matches[1] as $match)
  663. {
  664. // variable
  665. $variable = $this->getVariableString($match);
  666. // init vars
  667. $search = array();
  668. $replace = array();
  669. // search for
  670. $search[0] = '{cache:'. $match .'}';
  671. $search[1] = '{/cache:'. $match .'}';
  672. // replace with
  673. $replace[0] = "<?php if(!\$this->isCached(". $variable .")): ?>\n<?php ob_start(); ?>";
  674. $replace[1] = "<?php SpoonFile::setContent(\$this->cacheDirectory .'/'. $variable .'_cache.tpl', ob_get_clean()); ?>\n<?php endif; ?>\n";
  675. $replace[1] .= "<?php require \$this->cacheDirectory .'/'. $variable .'_cache.tpl'; ?>";
  676. // execute
  677. $content = str_replace($search, $replace, $content);
  678. }
  679. }
  680. return $content;
  681. }
  682. /**
  683. * Parses the cycle tags in the given content.
  684. *
  685. * @return string The updated content, containing the parsed cycle tags.
  686. * @param string $content The content that may contain the cycle tags.
  687. */
  688. private function parseCycle($content, $iteration)
  689. {
  690. // regex pattern
  691. $pattern = "|\{cycle((:\'[a-z0-9\-_\<\/\>\s]+\')+)\}|is";
  692. // find matches
  693. if(preg_match_all($pattern, $content, $matches))
  694. {
  695. // chunks
  696. $chunks = explode('.', $iteration);
  697. // number of chunks
  698. $numChunks = count($chunks);
  699. // variable string
  700. $variable = '$'. SpoonFilter::toCamelCase(str_replace(array('[', ']', "'", '_'), ' ', $chunks[$numChunks - 1]), ' ', true, SPOON_CHARSET);
  701. // loop matches
  702. foreach($matches[1] as $i => $match)
  703. {
  704. // correct cycle
  705. if($this->isCorrectSyntax($match, 'cycle'))
  706. {
  707. // init vars
  708. $cycle = '';
  709. $inParameter = false;
  710. $parameters = trim($match, ':');
  711. // loop every character
  712. for($c = 0; $c < mb_strlen($parameters, SPOON_CHARSET); $c++)
  713. {
  714. // fetch character
  715. $string = mb_substr($parameters, $c, 1, SPOON_CHARSET);
  716. // single quote in parameter, indicating the end for this parameter
  717. if($string == "'" && $inParameter) $inParameter = false;
  718. // single quotes, indicating the start of a new parameter
  719. elseif($string == "'" && !$inParameter) $inParameter = true;
  720. // semicolon outside parameter
  721. elseif($string == ':' && !$inParameter) $string = ', ';
  722. // add character
  723. $cycle .= $string;
  724. }
  725. // search & replace
  726. $search = $matches[0][$i];
  727. $replace = '<?php echo $this->cycle('. $variable .'I, array('. $cycle .')); ?>';
  728. // init var
  729. $iterations = array();
  730. /*
  731. * The name of this iteration may contain characters that need to be escaped if you
  732. * want to use them as a literal string in a regex match.
  733. */
  734. $iterationPattern = str_replace(array('.', '[', ']', "'"), array('\.', '\[', '\]', "\'"), $iteration);
  735. // match current iteration
  736. preg_match_all('|{iteration:'. $iterationPattern .'}.*{/iteration:'. $iterationPattern .'}|ismU', $content, $iterations);
  737. // loop mathes
  738. foreach($iterations as $block)
  739. {
  740. // build new content
  741. $newContent = str_replace($search, $replace, $block);
  742. // replace in original content
  743. $content = str_replace($block, $newContent, $content);
  744. }
  745. }
  746. }
  747. }
  748. return $content;
  749. }
  750. /**
  751. * Parse the forms.
  752. *
  753. * @return string The updated content, containing the parsed form tags.
  754. * @param string $content The content that may contain the form tags.
  755. */
  756. private function parseForms($content)
  757. {
  758. // regex pattern
  759. $pattern = '/\{form:([a-z0-9_]+?)\}?/siU';
  760. // find matches
  761. if(preg_match_all($pattern, $content, $matches))
  762. {
  763. // loop matches
  764. foreach($matches[1] as $name)
  765. {
  766. // form object with that name exists
  767. if(isset($this->forms[$name]))
  768. {
  769. // init vars
  770. $search = array();
  771. $replace = array();
  772. // start & close tag
  773. $search = array('{form:'. $name .'}', '{/form:'. $name .'}');
  774. $replace[0] = '<form action="<?php echo $this->forms[\''. $name .'\']->getAction(); ?>" method="<?php echo $this->forms[\''. $name .'\']->getMethod(); ?>"<?php echo $this->forms[\''. $name .'\']->getParametersHTML(); ?>>' ."\n<div>\n";
  775. $replace[0] .= $this->forms[$name]->getField('form')->parse();
  776. $replace[1] = "\n</div>\n</form>";
  777. $content = str_replace($search, $replace, $content);
  778. }
  779. }
  780. }
  781. return $content;
  782. }
  783. /**
  784. * Parse the include tags.
  785. *
  786. * @return string The updated content, containing the parsed include tags.
  787. * @param string $content The content that may contain the include tags.
  788. */
  789. private function parseIncludes($content)
  790. {
  791. // regex pattern
  792. $pattern = "|\{include:file='([a-z0-9\-_\.:\{\$\}\/]+)'\}|is";
  793. // find matches
  794. if(preg_match_all($pattern, $content, $matches))
  795. {
  796. // loop matches
  797. foreach($matches[1] as $match)
  798. {
  799. // file
  800. $file = $this->getVariableString($match); // @todo herwerken.
  801. // template path
  802. $template = eval('error_reporting(0); return '. $file .';');
  803. // search string
  804. $search = '{include:file=\''. $match .'\'}';
  805. // replace string
  806. $replace = '<?php if($this->getForceCompile()) $this->compile(\''. dirname(realpath($this->template)) .'\', '. $file .'); ?>' ."\n";
  807. $replace .= '<?php $return = @include $this->getCompileDirectory() .\'/\'. $this->getCompileName('. $file .',\''. dirname(realpath($this->template)) .'\'); ?>' ."\n";
  808. $replace .= '<?php if($return === false): ?>' ."\n";
  809. $replace .= '<?php $this->compile(\''. dirname(realpath($this->template)) .'\', '. $file .'); ?>' ."\n";
  810. $replace .= '<?php @include $this->getCompileDirectory() .\'/\'. $this->getCompileName('. $file .',\''. dirname(realpath($this->template)) .'\'); ?>' ."\n";
  811. $replace .= '<?php endif; ?>' ."\n";
  812. // replace it
  813. $content = str_replace($search, $replace, $content);
  814. }
  815. }
  816. return $content;
  817. }
  818. /**
  819. * Parse the iterations (recursively).
  820. *
  821. * @return string The updated content, containing the parsed iteration tags.
  822. * @param string $content The content that my contain the iteration tags.
  823. */
  824. private function parseIterations($content)
  825. {
  826. // fetch iterations
  827. $pattern = '/\{iteration:(([a-z09\'\[\]])+(\.([a-z0-9\'\[\]])+)?)\}/is';
  828. // find matches
  829. if(preg_match_all($pattern, $content, $matches))
  830. {
  831. // init var
  832. $iterations = array();
  833. // loop matches
  834. foreach($matches[1] as $match)
  835. {
  836. if(!in_array($match, $iterations)) $iterations[] = $match;
  837. }
  838. // has iterations
  839. if(count($iterations) != 0)
  840. {
  841. // loop iterations
  842. foreach($iterations as $iteration)
  843. {
  844. // check iteration syntax
  845. if($this->isCorrectSyntax($iteration, 'iteration'))
  846. {
  847. // parse cycle tag
  848. $content = $this->parseCycle($content, $iteration);
  849. // init vars
  850. $search = array();
  851. $replace = array();
  852. // search
  853. $search[0] = '{iteration:'. $iteration .'}';
  854. $search[1] = '{/iteration:'. $iteration .'}';
  855. // convert multiple dots to a single one
  856. $name = preg_replace('/\.+/', '.', $iteration);
  857. $name = trim($name, '.');
  858. // explode using the dots
  859. $chunks = explode('.', $name);
  860. // number of chunks
  861. $numChunks = count($chunks);
  862. // define variable
  863. $variable = $this->parseVariable($name);
  864. // internal variable
  865. $internalVariable = SpoonFilter::toCamelCase(str_replace(array('[', ']', "'", '_'), ' ', $chunks[$numChunks - 1]), ' ', true, SPOON_CHARSET);
  866. // replace
  867. $replace[0] = '<?php $'. $internalVariable .'Count = count('. $variable ."); ?>\n";
  868. $replace[0] .= '<?php foreach((array) '. $variable .' as $'. $internalVariable .'I => $'. $internalVariable ."): ?>\n";
  869. $replace[0] .= "<?php
  870. if(!isset(\$". $internalVariable ."['first']) && \$". $internalVariable ."I == 0) \$". $internalVariable ."['first'] = true;
  871. if(!isset(\$". $internalVariable ."['last']) && \$". $internalVariable ."I == \$". $internalVariable ."Count - 1) \$". $internalVariable ."['last'] = true;
  872. if(isset(\$". $internalVariable ."['formElements']) && is_array(\$". $internalVariable ."['formElements']))
  873. {
  874. foreach(\$". $internalVariable ."['formElements'] as \$name => \$object)
  875. {
  876. \$". $internalVariable ."[\$name] = \$object->parse();
  877. \$". $internalVariable ."[\$name .'Error'] = (\$object->getErrors() == '') ? '' : '<span class=\"formError\">'. \$object->getErrors() .'</span>';
  878. }
  879. }
  880. ?>";
  881. $replace[1] = '<?php endforeach; ?>';
  882. // replace
  883. $content = str_replace($search, $replace, $content);
  884. }
  885. }
  886. }
  887. }
  888. return $content;
  889. }
  890. /**
  891. * Parse the options in the given content & scope.
  892. *
  893. * @return string The updated content, containing the parsed option tags.
  894. * @param string $content The content that may contain the option tags.
  895. */
  896. private function parseOptions($content)
  897. {
  898. // regex pattern
  899. $pattern = "/\{option:((\!)?[a-z0-9\-_\[\]\']+(\.([a-z0-9\-_\[\]\'])+)?)}.*?\{\/option:\\1\}/is";
  900. // init vars
  901. $options = array();
  902. // keep finding those options!
  903. while(1)
  904. {
  905. // find matches
  906. if(preg_match_all($pattern, $content, $matches))
  907. {
  908. // init var
  909. $correctOptions = false;
  910. // loop matches
  911. foreach($matches[1] as $match)
  912. {
  913. // correct syntax
  914. if($this->isCorrectSyntax($match, 'option'))
  915. {
  916. // redefine match
  917. $match = str_replace('!', '', $match);
  918. // fetch variable
  919. $variable = $this->parseVariable($match);
  920. // already matched
  921. if(in_array($match, $options)) continue;
  922. // init vars
  923. $search = array();
  924. $replace = array();
  925. // not yet used
  926. $options[] = $match;
  927. // search for
  928. $search[] = '{option:'. $match .'}';
  929. $search[] = '{/option:'. $match .'}';
  930. // inverse option
  931. $search[] = '{option:!'. $match .'}';
  932. $search[] = '{/option:!'. $match .'}';
  933. // replace with
  934. $replace[] = '<?php if(isset('. $variable .') && count('. $variable .') != 0 && '. $variable .' != \'\' && '. $variable .' !== false): ?>';
  935. $replace[] = '<?php endif; ?>';
  936. // inverse option
  937. $replace[] = '<?php if(!isset('. $variable .') || count('. $variable .') == 0 || '. $variable .' == \'\' || '. $variable .' === false): ?>';
  938. $replace[] = '<?php endif; ?>';
  939. // go replace
  940. $content = str_replace($search, $replace, $content);
  941. // reset vars
  942. unset($search);
  943. unset($replace);
  944. // at least one correct option
  945. $correctOptions = true;
  946. }
  947. }
  948. // no correct options were found
  949. if(!$correctOptions) break;
  950. }
  951. // no matches
  952. else break;
  953. }
  954. return $content;
  955. }
  956. /**
  957. * Parse the template to a file.
  958. *
  959. * @return void
  960. */
  961. public function parseToFile()
  962. {
  963. SpoonFile::setContent($this->compileDirectory .'/'. $this->getCompileName($this->template), $this->getContent());
  964. }
  965. /**
  966. * Parse a single variable.
  967. *
  968. * @return string The variable as PHP code.
  969. * @param string $variable The variable that needs to be converted to php code.
  970. */
  971. private function parseVariable($variable)
  972. {
  973. // strip '{$' and '}'
  974. $variable = ltrim($variable, '{$');
  975. $variable = rtrim($variable, '}');
  976. // fetch modifiers
  977. $var = explode('|', $variable);
  978. // base variable
  979. $variable = '';
  980. // explode using the dots
  981. $varChunks = explode('.', $var[0]);
  982. // number of chunks
  983. $numChunks = count($varChunks);
  984. // more than 2 chunks is NOT allowed
  985. if($numChunks > 2) return '\'{$'. implode('|', $var) .'}\'';
  986. // 2 chunks
  987. elseif($numChunks == 2)
  988. {
  989. // contains [
  990. if(strpos($varChunks[1],'[') !== false)
  991. {
  992. // get rid of ]
  993. $varChunks[1] = str_replace(']', '', $varChunks[1]);
  994. // create chunks
  995. $bracketChunks = explode('[', $varChunks[1]);
  996. // add first part
  997. $variable = '$'. $varChunks[0];
  998. // loop all chunks
  999. for($i = 0; $i < count($bracketChunks); $i++)
  1000. {
  1001. // explicitly add single quotes for the first element
  1002. if($i == 0) $variable .= '[\''. $bracketChunks[$i] .'\']';
  1003. // everything after first as is provided in the template
  1004. else $variable .= '['. $bracketChunks[$i] .']';
  1005. }
  1006. }
  1007. // no square bracketes used
  1008. else $variable = '$'. $varChunks[0] .'[\''. $varChunks[1] .'\']';
  1009. }
  1010. // 1 chunk
  1011. else
  1012. {
  1013. // contains [
  1014. if(strpos($varChunks[0],'[') !== false)
  1015. {
  1016. // get rid of ]
  1017. $varChunks[0] = str_replace(']', '', $varChunks[0]);
  1018. // create chunks
  1019. $bracketChunks = explode('[', $varChunks[0]);
  1020. // add first part
  1021. $variable = '$this->variables[\''. $bracketChunks[0] .'\']';
  1022. // loop all chunks
  1023. for($i = 1; $i < count($bracketChunks); $i++)
  1024. {
  1025. // add this chunk (as provided in the template)
  1026. $variable .= '['. $bracketChunks[$i] .']';
  1027. }
  1028. }
  1029. // no square brackets used
  1030. else $variable = '$this->variables[\''. $var[0] .'\']';
  1031. }
  1032. // has modifiers ?
  1033. if(isset($var[1]))
  1034. {
  1035. // loop modifiers
  1036. foreach($var as $i => $modifier)
  1037. {
  1038. // skip first record
  1039. if($i == 0) continue;
  1040. // modifier + parameters
  1041. $modifierChunks = explode(':', $modifier);
  1042. // modifier doesn't exist
  1043. if(!isset($this->modifiers[$modifierChunks[0]])) throw new SpoonTemplateException('The modifier ('. $modifierChunks[0] .') does not exist.');
  1044. // add call
  1045. else
  1046. {
  1047. // method call
  1048. if(is_array($this->modifiers[$modifierChunks[0]])) $variable = implode('::', $this->modifiers[$modifierChunks[0]]) .'('. $variable;
  1049. // function call
  1050. else $variable = $this->modifiers[$modifierChunks[0]] .'('. $variable;
  1051. }
  1052. // has arguments
  1053. if(count($modifierChunks) > 1)
  1054. {
  1055. // init vars
  1056. $inParameter = false;
  1057. $parameters = mb_substr($modifier, strlen($modifierChunks[0]), mb_strlen($modifier, SPOON_CHARSET), SPOON_CHARSET);
  1058. // loop every character
  1059. for($i = 0; $i < mb_strlen($parameters, SPOON_CHARSET); $i++)
  1060. {
  1061. // fetch character
  1062. $string = mb_substr($parameters, $i, 1, SPOON_CHARSET);
  1063. // single quote in parameter, indicating the end for this parameter
  1064. if($string == "'" && $inParameter) $inParameter = false;
  1065. // single quotes, indicating the start of a new parameter
  1066. elseif($string == "'" && !$inParameter) $inParameter = true;
  1067. // semicolon outside parameter
  1068. elseif($string == ':' && !$inParameter) $string = ', ';
  1069. // add character
  1070. $variable .= $string;
  1071. }
  1072. }
  1073. // add close tag
  1074. $variable .= ')';
  1075. }
  1076. }
  1077. return $variable;
  1078. }
  1079. /**
  1080. * Parse all the variables in this string.
  1081. *
  1082. * @return string The updated content, containing the parsed variables.
  1083. * @param string $content The content that may contain variables.
  1084. */
  1085. private function parseVariables($content)
  1086. {
  1087. // regex pattern
  1088. $pattern = '/\{\$([a-z0-9_\'\[\]])+(\.([a-z0-9_\'\[\]])+)?(\|[a-z0-9\-_]+(:[\']?[a-z0-9\-_\s\$\[\]:]+[\']?)*)*\}/i';
  1089. // temp variables
  1090. $variables = array();
  1091. /*
  1092. * We willen een lijstje bijhouden van alle variabelen die wel gematched zijn, maar niet correct zijn.
  1093. * Van zodra dit de enige variabelen zijn die nog overschieten, dang aan we de while loop breken.
  1094. */
  1095. // we want to keep parsing vars until none can be found.
  1096. while(1)
  1097. {
  1098. // find matches
  1099. if(preg_match_all($pattern, $content, $matches))
  1100. {
  1101. // init var
  1102. $correctVariables = false;
  1103. // loop matches
  1104. foreach($matches[0] as $match)
  1105. {
  1106. // variable doesn't already exist
  1107. if(array_search($match, $variables, true) === false)
  1108. {
  1109. // syntax check this match
  1110. if($this->isCorrectSyntax($match, 'variable'))
  1111. {
  1112. // unique key
  1113. $key = md5($match);
  1114. // add parsed variable
  1115. $variables[$key] = $this->parseVariable($match);
  1116. // replace in content
  1117. $content = str_replace($match, '[$'. $key .']', $content);
  1118. // note that at least 1 good variable was found
  1119. $correctVariables = true;
  1120. }
  1121. }
  1122. }
  1123. if(!$correctVariables) break;
  1124. }
  1125. // break the loop, no matches were found
  1126. else break;
  1127. }
  1128. /**
  1129. * Every variable needs to be searched & replaced one by one,
  1130. * since only then the nesting process works as intended.
  1131. */
  1132. foreach($variables as $key => $value)
  1133. {
  1134. // loop each element except this one
  1135. foreach($variables as $keyIndex => $valueContent)
  1136. {
  1137. // skip myself
  1138. if($key == $keyIndex) continue;
  1139. // replace myself in the other var
  1140. $variables[$keyIndex] = str_replace('[$'. $key .']', $variables[$key], $variables[$keyIndex]);
  1141. }
  1142. }
  1143. /**
  1144. * Now loop these vars again, but this time parse them in the
  1145. * content we're actually working with.
  1146. */
  1147. foreach($variables as $key => $value)
  1148. {
  1149. $content = str_replace('[$'. $key .']', '<?php echo '. $value .'; ?>', $content);
  1150. }
  1151. return $content;
  1152. }
  1153. /**
  1154. * Set the cache directory.
  1155. *
  1156. * @return void
  1157. * @param string $path The location of the cache directory to store cached template blocks.
  1158. */
  1159. public function setCacheDirectory($path)
  1160. {
  1161. $this->cacheDirectory = (string) $path;
  1162. }
  1163. /**
  1164. * Set the compile directory.
  1165. *
  1166. * @return void
  1167. * @param string $path The location of the compile directory to store compiled templates in.
  1168. */
  1169. public function setCompileDirectory($path)
  1170. {
  1171. $this->compileDirectory = (string) $path;
  1172. }
  1173. /**
  1174. * If enabled, recompiles a template even if it has already been compiled.
  1175. *
  1176. * @return void
  1177. * @param bool[optional] $on Should this template be recompiled every time it's loaded.
  1178. */
  1179. public function setForceCompile($on = true)
  1180. {
  1181. $this->foreCompile = (bool) $on;
  1182. }
  1183. /**
  1184. * Sets the forms.
  1185. *
  1186. * @return void
  1187. * @param array $forms An array of forms that need to be included in this template.
  1188. */
  1189. public function setForms(array $forms)
  1190. {
  1191. $this->forms = $forms;
  1192. }
  1193. /**
  1194. * Strips php code from the content.
  1195. *
  1196. * @return string The updated content, no longer containing php code.
  1197. * @param string $content The content that may contain php code.
  1198. */
  1199. private function stripCode($content)
  1200. {
  1201. return $content = preg_replace("/\<\?(php)?(.*)\?\>/siU", '', $content);
  1202. }
  1203. /**
  1204. * Strip comments from the output.
  1205. *
  1206. * @return string The updated content, no longer containing template comments.
  1207. * @param string $content The content that may contain template comments.
  1208. */
  1209. private function stripComments($content)
  1210. {
  1211. return $content = preg_replace("/\{\*(.+?)\*\}/s", '', $content);
  1212. }
  1213. }
  1214. /**
  1215. * This class implements modifier mapping for the template engine.
  1216. *
  1217. * @package template
  1218. *
  1219. *
  1220. * @author Davy Hellemans <davy@spoon-library.be>
  1221. * @since 1.0.0
  1222. */
  1223. class SpoonTemplateModifiers
  1224. {
  1225. /**
  1226. * Default modifiers mapped to their functions
  1227. *
  1228. * @var array
  1229. */
  1230. private static $modifiers = array( 'addslashes' => 'addslashes',
  1231. 'createhtmllinks' => array('SpoonTemplateModifiers', 'createHTMLLinks'),
  1232. 'date' => array('SpoonTemplateModifiers', 'date'),
  1233. 'htmlentities' => array('SpoonFilter', 'htmlentities'),
  1234. 'lowercase' => array('SpoonTemplateModifiers', 'lowercase'),
  1235. 'ltrim' => 'ltrim',
  1236. 'nl2br' => 'nl2br',
  1237. 'repeat' => 'str_repeat',
  1238. 'rtrim' => 'rtrim',
  1239. 'shuffle' => 'str_shuffle',
  1240. 'sprintf' => 'sprintf',
  1241. 'stripslashes' => 'stripslashes',
  1242. 'substring' => 'substr',
  1243. 'trim' => 'trim',
  1244. 'ucfirst' => 'ucfirst',
  1245. 'ucwords' => 'ucwords',
  1246. 'uppercase' => array('SpoonTemplateModifiers', 'uppercase'));
  1247. /**
  1248. * Clears the entire modifiers list.
  1249. *
  1250. * @return void
  1251. */
  1252. public static function clearModifiers()
  1253. {
  1254. self::$modifiers = array();
  1255. }
  1256. /**
  1257. * Converts links to HTML links (only to be used with cleartext).
  1258. *
  1259. * @return string The text containing the parsed html links.
  1260. * @param string $text The cleartext that may contain urls that need to be transformed to html links.
  1261. */
  1262. public static function createHTMLLinks($text)
  1263. {
  1264. return SpoonFilter::replaceURLsWithAnchors($text, false);
  1265. }
  1266. /**
  1267. * Formats a language specific date.
  1268. *
  1269. * @return string The formatted date according to the timestamp, format and provided language.
  1270. * @param int $timestamp The timestamp that you want to apply the format to.
  1271. * @param string[optional] $format The optional format that you want to apply on the provided timestamp.
  1272. * @param string[optional] $language The optional language that you want this format in. (Check SpoonLocale for the possible languages)
  1273. */
  1274. public static function date($timestamp, $format = 'Y-m-d H:i:s', $language = 'en')
  1275. {
  1276. return SpoonDate::getDate($format, $timestamp, $language);
  1277. }
  1278. /**
  1279. * Retrieves the modifiers.
  1280. *
  1281. * @return array The list of modifiers and the function/method that they're mapped to.
  1282. */
  1283. public static function getModifiers()
  1284. {
  1285. return self::$modifiers;
  1286. }
  1287. /**
  1288. * Makes this string lowercase.
  1289. *
  1290. * @return string The string, completely lowercased.
  1291. * @param string $string The string that you want to apply this method on.
  1292. */
  1293. public static function lowercase($string)
  1294. {
  1295. return mb_convert_case($string, MB_CASE_LOWER, SPOON_CHARSET);
  1296. }
  1297. /**
  1298. * Maps a specific modifier to a function/method.
  1299. *
  1300. * @return void
  1301. * @param string $name The name of the modifier that you want to map.
  1302. * @param mixed $function The function or method that you want to map to the provided name. To map a method provided this argument as an array containing class and method.
  1303. */
  1304. public static function mapModifier($name, $function)
  1305. {
  1306. // validate modifier
  1307. if(!SpoonFilter::isValidAgainstRegexp('/[a-zA-Z0-9\_\-]+/', (string) $name)) throw new SpoonTemplateException('Modifier names can only contain a-z, 0-9 and - and _');
  1308. // class method
  1309. if(is_array($function))
  1310. {
  1311. // not enough elements
  1312. if(count($function) != 2) throw new SpoonTemplateException('The array should contain the class and static method.');
  1313. // method doesn't exist
  1314. if(!method_exists($function[0], $function[1])) throw new SpoonTemplateException('The method "'. $function[1] .'" in the class '. $function[0] .' does not exist.');
  1315. // all fine
  1316. self::$modifiers[(string) $name] = $function;
  1317. }
  1318. // regular function
  1319. else
  1320. {
  1321. // function doesn't exist
  1322. if(!function_exists((string) $function)) throw new SpoonTemplateException('The function "'. (string) $function .'" does not exist.');
  1323. // all fine
  1324. self::$modifiers[(string) $name] = $function;
  1325. }
  1326. }
  1327. /**
  1328. * Transform the string to uppercase.
  1329. *
  1330. * @return string The string, completly uppercased.
  1331. * @param string $string The string that you want to apply this method on.
  1332. */
  1333. public static function uppercase($string)
  1334. {
  1335. return mb_convert_case($string, MB_CASE_UPPER, SPOON_CHARSET);
  1336. }
  1337. }
  1338. ?>