PageRenderTime 57ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/library/spoon/template/compiler.php

http://github.com/forkcms/forkcms
PHP | 1045 lines | 494 code | 177 blank | 374 comment | 52 complexity | e3f7738c069ef3a7526f3a8c7caea4ba MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, MIT, AGPL-3.0, LGPL-2.1, 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.com
  7. *
  8. * @package spoon
  9. * @subpackage template
  10. *
  11. *
  12. * @author Davy Hellemans <davy@spoon-library.com>
  13. * @since 0.1.1
  14. */
  15. /**
  16. * Spoon Library
  17. *
  18. * This source file is part of the Spoon Library. More information,
  19. * documentation and tutorials can be found @ http://www.spoon-library.com
  20. *
  21. * @package spoon
  22. * @subpackage template
  23. *
  24. *
  25. * @author Davy Hellemans <davy@spoon-library.com>
  26. * @author Matthias Mullie <matthias@spoon-library.com>
  27. * @author Tijs Verkoyen <tijs@spoon-library.com>
  28. * @since 1.3.0
  29. */
  30. class SpoonTemplateCompiler
  31. {
  32. /**
  33. * Cache directory location
  34. *
  35. * @var string
  36. */
  37. protected $cacheDirectory = '.';
  38. /**
  39. * Compile directory location
  40. *
  41. * @var string
  42. */
  43. protected $compileDirectory = '.';
  44. /**
  45. * Working content
  46. *
  47. * @var string
  48. */
  49. protected $content;
  50. /**
  51. * Always recompile
  52. *
  53. * @var bool
  54. */
  55. protected $forceCompile = false;
  56. /**
  57. * List of form objects
  58. *
  59. * @var array
  60. */
  61. protected $forms = array();
  62. /**
  63. * List of used iterations
  64. *
  65. * @var array
  66. */
  67. protected $iterations = array();
  68. /**
  69. * Counter of used iterations (each iteration will get a unique number)
  70. *
  71. * @var int
  72. */
  73. protected $iterationsCounter;
  74. /**
  75. * Cached list of the modifiers
  76. *
  77. * @var array
  78. */
  79. protected $modifiers = array();
  80. /**
  81. * Is the content already parsed
  82. *
  83. * @var bool
  84. */
  85. protected $parsed = false;
  86. /**
  87. * Template file
  88. *
  89. * @var string
  90. */
  91. protected $template;
  92. /**
  93. * List of compiler-interpreted variables
  94. *
  95. * @var array
  96. */
  97. protected $templateVariables = array();
  98. /**
  99. * List of variables
  100. *
  101. * @var array
  102. */
  103. protected $variables = array();
  104. /**
  105. * Class constructor.
  106. *
  107. * @param string $template The name of the template to compile.
  108. * @param array $variables The list of possible variables.
  109. */
  110. public function __construct($template, array $variables)
  111. {
  112. $this->template = (string) $template;
  113. $this->variables = $variables;
  114. }
  115. /**
  116. * Retrieve the compiled name for this template.
  117. *
  118. * @return string The unique filename used to store the compiled template in the compile directory.
  119. * @param string $template The name of the template.
  120. */
  121. protected function getCompileName($template)
  122. {
  123. return md5(realpath($template)) . '_' . basename($template) . '.php';
  124. }
  125. /**
  126. * Retrieve the content.
  127. *
  128. * @return string The php compiled template.
  129. */
  130. public function getContent()
  131. {
  132. if(!$this->parsed) $this->parse();
  133. return $this->content;
  134. }
  135. /**
  136. * Parse the template.
  137. */
  138. protected function parse()
  139. {
  140. // not yet parsed
  141. if(!$this->parsed)
  142. {
  143. // while developing, you might want to know about the undefined indexes
  144. $errorReporting = (SPOON_DEBUG) ? 'E_ALL | E_STRICT' : 'E_WARNING';
  145. $displayErrors = (SPOON_DEBUG) ? 'On' : 'Off';
  146. // add to the list of parsed files
  147. $this->files[] = $this->getCompileName($this->template);
  148. // map modifiers
  149. $this->modifiers = SpoonTemplateModifiers::getModifiers();
  150. // set content
  151. $this->content = SpoonFile::getContent($this->template);
  152. // strip php code
  153. $this->content = $this->stripCode($this->content);
  154. // strip comments
  155. $this->content = $this->stripComments($this->content);
  156. // prepare iterations
  157. $this->content = $this->prepareIterations($this->content);
  158. // parse iterations
  159. $this->content = $this->parseIterations($this->content);
  160. // parse variables
  161. $this->content = $this->parseVariables($this->content);
  162. // parse options
  163. $this->content = $this->parseOptions($this->content);
  164. // includes
  165. $this->content = $this->parseIncludes($this->content);
  166. // parse cache tags
  167. $this->content = $this->parseCache($this->content);
  168. // parse forms
  169. $this->content = $this->parseForms($this->content);
  170. // replace variables
  171. $this->content = $this->replaceVariables($this->content);
  172. // add error_reporting setting
  173. $this->content = '<?php error_reporting(' . $errorReporting . '); ini_set(\'display_errors\', \'' . $displayErrors . '\'); ?>' . "\n" . $this->content;
  174. // parsed
  175. $this->parsed = true;
  176. }
  177. }
  178. /**
  179. * Parse the cache tags.
  180. *
  181. * @return string The updated content, containing the parsed cache tags.
  182. * @param string $content The content that may contain the parse tags.
  183. */
  184. protected function parseCache($content)
  185. {
  186. // regex pattern
  187. $pattern = '/\{cache:(.*?)}.*?\{\/cache:\\1\}/is';
  188. // find matches
  189. if(preg_match_all($pattern, $content, $matches))
  190. {
  191. // loop matches
  192. foreach($matches[1] as $match)
  193. {
  194. // search
  195. $search[0] = '{cache:' . $match . '}';
  196. $search[1] = '{/cache:' . $match . '}';
  197. // replace
  198. $replace[0] = '<?php
  199. ob_start();
  200. ?>' . $match . '<?php
  201. $cache = eval(\'return \\\'\' . str_replace(\'\\\'\', \'\\\\\\\'\', ob_get_clean()) .\'\\\';\');
  202. if(!$this->isCached($cache))
  203. {
  204. ob_start();
  205. ?>';
  206. $replace[1] = '<?php
  207. SpoonFile::setContent($this->cacheDirectory .\'/\' . $cache .\'_cache.tpl\', ob_get_clean());
  208. }
  209. require $this->cacheDirectory .\'/\' . $cache .\'_cache.tpl\';
  210. ?>';
  211. // replace it
  212. $content = str_replace($search, $replace, $content);
  213. }
  214. }
  215. return $content;
  216. }
  217. /**
  218. * Parses the cycle tags in the given content.
  219. *
  220. * @return string The updated content, containing the parsed cycle tags.
  221. * @param string $content The content that may contain the cycle tags.
  222. * @param string $iteration The iteration.
  223. */
  224. protected function parseCycle($content, $iteration)
  225. {
  226. // regex pattern
  227. $pattern = '/\{cycle((:(.*?))+)\}/is';
  228. // find matches
  229. if(preg_match_all($pattern, $content, $matches, PREG_SET_ORDER))
  230. {
  231. // loop matches
  232. foreach($matches as $i => $match)
  233. {
  234. // cycles pattern
  235. $pattern = '/:("[^"]*?"|\'[^\']*?\'|[^:]*)/';
  236. // has cycles
  237. if(preg_match_all($pattern, $match[1], $arguments))
  238. {
  239. // init search & replace: reset arguments array
  240. $search = $match[0];
  241. $replace = '<?php
  242. $arguments = array();';
  243. // loop arguments
  244. foreach($arguments[1] as &$argument)
  245. {
  246. // inside a string
  247. if(in_array(substr($argument, 0, 1), array('\'', '"')))
  248. {
  249. // strip quotes
  250. $argument = substr($argument, 1, -1);
  251. }
  252. // let's do this argument per argument: we need our eval to take care of the esacping (parsed variables result may contain single quotes)
  253. $replace .= '
  254. ob_start();
  255. ?>' . $argument . '<?php
  256. $arguments[] = eval(\'return \\\'\' . str_replace(\'\\\'\', \'\\\\\\\'\', ob_get_clean()) .\'\\\';\');';
  257. }
  258. // finish replace: create cycle
  259. $replace .= '
  260. echo $this->cycle(' . $iteration . '[\'i\'], $arguments);
  261. ?>' . "\n";
  262. // replace it
  263. $content = str_replace($search, $replace, $content);
  264. }
  265. }
  266. }
  267. return $content;
  268. }
  269. /**
  270. * Parse the forms.
  271. *
  272. * @return string The updated content, containing the parsed form tags.
  273. * @param string $content The content that may contain the form tags.
  274. */
  275. protected function parseForms($content)
  276. {
  277. // regex pattern
  278. $pattern = '/\{form:([a-z0-9_]+?)\}?/siU';
  279. // find matches
  280. if(preg_match_all($pattern, $content, $matches))
  281. {
  282. // loop matches
  283. foreach($matches[1] as $name)
  284. {
  285. // init vars
  286. $search = array();
  287. $replace = array();
  288. // start & close tag
  289. $search = array('{form:' . $name . '}', '{/form:' . $name . '}');
  290. // using UTF-8 as charset
  291. if(SPOON_CHARSET == 'utf-8')
  292. {
  293. $replace[0] = '<?php
  294. if(isset($this->forms[\'' . $name . '\']))
  295. {
  296. ?><form accept-charset="UTF-8" action="<?php echo $this->forms[\'' . $name . '\']->getAction(); ?>" method="<?php echo $this->forms[\'' . $name . '\']->getMethod(); ?>"<?php echo $this->forms[\'' . $name . '\']->getParametersHTML(); ?>>
  297. <?php echo $this->forms[\'' . $name . '\']->getField(\'form\')->parse();
  298. if($this->forms[\'' . $name . '\']->getUseToken())
  299. {
  300. ?><input type="hidden" name="form_token" id="<?php echo $this->forms[\'' . $name . '\']->getField(\'form_token\')->getAttribute(\'id\'); ?>" value="<?php echo $this->forms[\'' . $name . '\']->getField(\'form_token\')->getValue(); ?>" />
  301. <?php } ?>';
  302. }
  303. // no UTF-8
  304. else
  305. {
  306. $replace[0] = '<?php
  307. if(isset($this->forms[\'' . $name . '\']))
  308. {
  309. ?><form action="<?php echo $this->forms[\'' . $name . '\']->getAction(); ?>" method="<?php echo $this->forms[\'' . $name . '\']->getMethod(); ?>"<?php echo $this->forms[\'' . $name . '\']->getParametersHTML(); ?>>
  310. <?php echo $this->forms[\'' . $name . '\']->getField(\'form\')->parse();
  311. if($this->forms[\'' . $name . '\']->getUseToken())
  312. {
  313. ?><input type="hidden" name="form_token" id="<?php echo $this->forms[\'' . $name . '\']->getField(\'form_token\')->getAttribute(\'id\'); ?>" value="<?php echo $this->forms[\'' . $name . '\']->getField(\'form_token\')->getValue(); ?>" />
  314. <?php } ?>';
  315. }
  316. $replace[1] = '</form>
  317. <?php } ?>';
  318. $content = str_replace($search, $replace, $content);
  319. }
  320. }
  321. return $content;
  322. }
  323. /**
  324. * Parse the include tags.
  325. *
  326. * @return string The updated content, containing the parsed include tags.
  327. * @param string $content The content that may contain the include tags.
  328. */
  329. protected function parseIncludes($content)
  330. {
  331. // regex pattern
  332. // no unified restriction can be done on the allowed characters, that differs from one OS to another (see http://www.comentum.com/File-Systems-HFS-FAT-UFS.html)
  333. $pattern = '/\{include:(("[^"]*?"|\'[^\']*?\')|[^:]*?)\}/i';
  334. // find matches
  335. if(preg_match_all($pattern, $content, $matches, PREG_SET_ORDER))
  336. {
  337. // loop matches
  338. foreach($matches as $match)
  339. {
  340. // search string
  341. $search = $match[0];
  342. // inside a string
  343. if(in_array(substr($match[1], 0, 1), array('\'', '"')))
  344. {
  345. // strip quotes
  346. $match[1] = substr($match[1], 1, -1);
  347. }
  348. // replace string
  349. $replace = '<?php
  350. ob_start();
  351. ?>' . $match[1] . '<?php
  352. $include = eval(\'return \\\'\' . str_replace(\'\\\'\', \'\\\\\\\'\', ob_get_clean()) .\'\\\';\');
  353. if($this->getForceCompile()) $this->compile(\'' . dirname(realpath($this->template)) . '\', $include);
  354. $return = @include $this->getCompileDirectory() .\'/\' . $this->getCompileName($include, \'' . dirname(realpath($this->template)) . '\');
  355. if($return === false && $this->compile(\'' . dirname(realpath($this->template)) . '\', $include))
  356. {
  357. $return = @include $this->getCompileDirectory() .\'/\' . $this->getCompileName($include, \'' . dirname(realpath($this->template)) . '\');
  358. }' . "\n";
  359. if(SPOON_DEBUG) $replace .= 'if($return === false)
  360. {
  361. ?>' . $match[0] . '<?php
  362. }' . "\n";
  363. $replace .= '?>';
  364. // replace it
  365. $content = str_replace($search, $replace, $content);
  366. }
  367. }
  368. return $content;
  369. }
  370. /**
  371. * Parse the iterations (recursively).
  372. *
  373. * @return string The updated content, containing the parsed iteration tags.
  374. * @param string $content The content that may contain the iteration tags.
  375. */
  376. protected function parseIterations($content)
  377. {
  378. // fetch iterations
  379. $pattern = '/(\{iteration_([0-9]+):([a-z0-9_]*)((\.[a-z0-9_]*)*)((-\>[a-z0-9_]*((\.[a-z0-9_]*)*))?)\})(.*?)(\{\/iteration_\\2:\\3\\4\\6\})/is';
  380. // find matches
  381. if(preg_match_all($pattern, $content, $matches, PREG_SET_ORDER))
  382. {
  383. // loop iterations
  384. foreach($matches as $match)
  385. {
  386. // base variable names
  387. $iteration = '$this->iterations[\'' . $this->getCompileName($this->template) . '_' . $match[2] . '\']';
  388. $internalVariable = '${\'' . $match[3] . '\'}';
  389. // variable within iteration
  390. if($match[6] != '')
  391. {
  392. // base
  393. $variable = '${\'' . $match[3] . '\'}';
  394. // add separate chunks
  395. foreach(explode('.', ltrim($match[4] . str_replace('->', '.', $match[6]), '.')) as $chunk)
  396. {
  397. // make sure it's a valid chunk
  398. if(!$chunk) continue;
  399. // append pieces
  400. $variable .= "['" . $chunk . "']";
  401. $iteration .= "['" . $chunk . "']";
  402. $internalVariable .= "['" . $chunk . "']";
  403. }
  404. }
  405. // regular variable
  406. else
  407. {
  408. // base
  409. $variable = '$this->variables[\'' . $match[3] . '\']';
  410. // add separate chunks
  411. foreach(explode('.', ltrim($match[4], '.')) as $chunk)
  412. {
  413. // make sure it's a valid chunk
  414. if(!$chunk) continue;
  415. // append pieces
  416. $variable .= "['" . $chunk . "']";
  417. $iteration .= "['" . $chunk . "']";
  418. $internalVariable .= "['" . $chunk . "']";
  419. }
  420. }
  421. // iteration content
  422. $innerContent = $match[10];
  423. // alter inner iterations, options & variables to indicate where the iteration starting point is
  424. $search = $match[3] . $match[4] . str_replace('->', '.', $match[6]) . '.';
  425. $replace = $match[3] . $match[4] . str_replace('->', '.', $match[6]) . '->';
  426. $innerContent = preg_replace('/\{(.*?)' . preg_quote($search, '/') . '(.*?)\}/is', '{\\1' . $replace . '\\2}', $innerContent);
  427. // parse inner iterations (recursively)
  428. $innerContent = $this->parseIterations($innerContent);
  429. // parse inner variables (they have to be parsed first in case a variable exists inside a cycle)
  430. $innerContent = $this->parseVariables($innerContent);
  431. // parse cycle tags
  432. $innerContent = $this->parseCycle($innerContent, $iteration);
  433. // start iteration
  434. $templateContent = '<?php';
  435. if(SPOON_DEBUG)
  436. {
  437. $templateContent .= '
  438. if(!isset(' . $variable . '))
  439. {
  440. ?>{iteration:' . $match[3] . $match[4] . $match[6] . '}<?php
  441. ' . $variable . ' = array();
  442. ' . $iteration . '[\'fail\'] = true;
  443. }';
  444. }
  445. $templateContent .= '
  446. if(isset(' . $internalVariable . ')) ' . $iteration . '[\'old\'] = ' . $internalVariable . ';
  447. ' . $iteration . '[\'iteration\'] = ' . $variable . ';
  448. ' . $iteration . '[\'i\'] = 1;
  449. ' . $iteration . '[\'count\'] = count(' . $iteration . '[\'iteration\']);
  450. foreach((array) ' . $iteration . '[\'iteration\'] as ' . $internalVariable . ')
  451. {
  452. if(!isset(' . $internalVariable . '[\'first\']) && ' . $iteration . '[\'i\'] == 1) ' . $internalVariable . '[\'first\'] = true;
  453. if(!isset(' . $internalVariable . '[\'last\']) && ' . $iteration . '[\'i\'] == ' . $iteration . '[\'count\']) ' . $internalVariable . '[\'last\'] = true;
  454. if(isset(' . $internalVariable . '[\'formElements\']) && is_array(' . $internalVariable . '[\'formElements\']))
  455. {
  456. foreach(' . $internalVariable . '[\'formElements\'] as $name => $object)
  457. {
  458. ' . $internalVariable . '[$name] = $object->parse();
  459. ' . $internalVariable . '[$name .\'Error\'] = (is_callable(array($object, \'getErrors\')) && $object->getErrors() != \'\') ? \'<span class="formError">\' . $object->getErrors() .\'</span>\' : \'\';
  460. }
  461. } ?>';
  462. // append inner content
  463. $templateContent .= $innerContent;
  464. // close iteration
  465. $templateContent .= '<?php
  466. ' . $iteration . '[\'i\']++;
  467. }';
  468. if(SPOON_DEBUG)
  469. {
  470. $templateContent .= '
  471. if(isset(' . $iteration . '[\'fail\']) && ' . $iteration . '[\'fail\'] == true)
  472. {
  473. ?>{/iteration:' . $match[3] . $match[4] . $match[6] . '}<?php
  474. }';
  475. }
  476. $templateContent .= '
  477. if(isset(' . $iteration . '[\'old\'])) ' . $internalVariable . ' = ' . $iteration . '[\'old\'];
  478. else unset(' . $iteration . ');
  479. ?>';
  480. // replace!
  481. $content = str_replace($match[0], $templateContent, $content);
  482. }
  483. }
  484. return $content;
  485. }
  486. /**
  487. * Parse the options in the given content & scope.
  488. *
  489. * @return string The updated content, containing the parsed option tags.
  490. * @param string $content The content that may contain the option tags.
  491. */
  492. protected function parseOptions($content)
  493. {
  494. // regex pattern
  495. $pattern = '/\{option:(\!?)([a-z0-9_]*)((\.[a-z0-9_]*)*)(-\>[a-z0-9_]*((\.[a-z0-9_]*)*))?}.*?\{\/option:\\1\\2\\3\\5?\}/is';
  496. // init vars
  497. $options = array();
  498. // keep finding those options!
  499. while(1)
  500. {
  501. // find matches
  502. if(preg_match_all($pattern, $content, $matches, PREG_SET_ORDER))
  503. {
  504. // loop matches
  505. foreach($matches as $match)
  506. {
  507. // variable within iteration
  508. if(isset($match[5]) && $match[5] != '')
  509. {
  510. // base
  511. $variable = '${\'' . $match[2] . '\'}';
  512. // add separate chunks
  513. foreach(explode('.', ltrim($match[3] . str_replace('->', '.', $match[5]), '.')) as $chunk)
  514. {
  515. $variable .= "['" . $chunk . "']";
  516. }
  517. }
  518. // regular variable
  519. else
  520. {
  521. // base
  522. $variable = '$this->variables';
  523. // add separate chunks
  524. foreach(explode('.', $match[2] . $match[3]) as $chunk)
  525. {
  526. $variable .= "['" . $chunk . "']";
  527. }
  528. }
  529. // init vars
  530. $search = array();
  531. $replace = array();
  532. // not yet used
  533. $options[] = $match;
  534. // set option
  535. $option = $match[2] . $match[3] . (isset($match[5]) ? $match[5] : '');
  536. // search for
  537. $search[] = '{option:' . $option . '}';
  538. $search[] = '{/option:' . $option . '}';
  539. // inverse option
  540. $search[] = '{option:!' . $option . '}';
  541. $search[] = '{/option:!' . $option . '}';
  542. // replace with
  543. $replace[] = '<?php
  544. if(isset(' . $variable . ') && count(' . $variable . ') != 0 && ' . $variable . ' != \'\' && ' . $variable . ' !== false)
  545. {
  546. ?>';
  547. $replace[] = '<?php } ?>';
  548. // inverse option
  549. $replace[] = '<?php if(!isset(' . $variable . ') || count(' . $variable . ') == 0 || ' . $variable . ' == \'\' || ' . $variable . ' === false): ?>';
  550. $replace[] = '<?php endif; ?>';
  551. // go replace
  552. $content = str_replace($search, $replace, $content);
  553. // reset vars
  554. unset($search);
  555. unset($replace);
  556. }
  557. }
  558. // no matches
  559. else break;
  560. }
  561. return $content;
  562. }
  563. /**
  564. * Parse the template to a file.
  565. */
  566. public function parseToFile()
  567. {
  568. SpoonFile::setContent($this->compileDirectory . '/' . $this->getCompileName($this->template), $this->getContent());
  569. }
  570. /**
  571. * Parse all the variables in this string.
  572. *
  573. * @return string The updated content, containing the parsed variables.
  574. * @param string $content The content that may contain variables.
  575. */
  576. protected function parseVariables($content)
  577. {
  578. // we want to keep parsing vars until none can be found
  579. while(1)
  580. {
  581. // regex pattern
  582. $pattern = '/\{\$([a-z0-9_]*)((\.[a-z0-9_]*)*)(-\>[a-z0-9_]*((\.[a-z0-9_]*)*))?((\|[a-z_][a-z0-9_]*(:.*?)*)*)\}/i';
  583. // find matches
  584. if(preg_match_all($pattern, $content, $matches, PREG_SET_ORDER))
  585. {
  586. // loop matches
  587. foreach($matches as $match)
  588. {
  589. // check if no variable has been used inside our variable (as argument for a modifier)
  590. $test = substr($match[0], 1);
  591. $testContent = $this->parseVariables($test);
  592. // inner variable found
  593. if($test != $testContent)
  594. {
  595. // variable has been parsed, so change the content to reflect this
  596. $content = str_replace($match[0], substr($match[0], 0, 1) . $testContent, $content);
  597. // next variable please
  598. continue;
  599. }
  600. // no inner variable found
  601. else
  602. {
  603. // variable doesn't already exist
  604. if(array_search($match[0], $this->templateVariables, true) === false)
  605. {
  606. // unique key
  607. $varKey = md5($match[0]);
  608. // variable within iteration
  609. if(isset($match[4]) && $match[4] != '')
  610. {
  611. // base
  612. $variable = '${\'' . $match[1] . '\'}';
  613. // add separate chunks
  614. foreach(explode('.', ltrim($match[2] . str_replace('->', '.', $match[4]), '.')) as $chunk)
  615. {
  616. $variable .= "['" . $chunk . "']";
  617. }
  618. }
  619. // regular variable
  620. else
  621. {
  622. // base
  623. $variable = '$this->variables';
  624. // add separate chunks
  625. foreach(explode('.', $match[1] . $match[2]) as $chunk)
  626. {
  627. $variable .= "['" . $chunk . "']";
  628. }
  629. }
  630. // save PHP code
  631. $PHP = $variable;
  632. // has modifiers
  633. if(isset($match[7]) && $match[7] != '')
  634. {
  635. // modifier pattern
  636. $pattern = '/\|([a-z_][a-z0-9_]*)((:("[^"]*?"|\'[^\']*?\'|[^:|]*))*)/i';
  637. // has match
  638. if(preg_match_all($pattern, $match[7], $modifiers))
  639. {
  640. // loop modifiers
  641. foreach($modifiers[1] as $key => $modifier)
  642. {
  643. // modifier doesn't exist
  644. if(!isset($this->modifiers[$modifier])) throw new SpoonTemplateException('The modifier "' . $modifier . '" does not exist.');
  645. // add call
  646. else
  647. {
  648. // method call
  649. if(is_array($this->modifiers[$modifier])) $PHP = implode('::', $this->modifiers[$modifier]) . '(' . $PHP;
  650. // function call
  651. else $PHP = $this->modifiers[$modifier] . '(' . $PHP;
  652. }
  653. // has arguments
  654. if($modifiers[2][$key] != '')
  655. {
  656. // arguments pattern (don't just explode on ':', it might be used inside a string argument)
  657. $pattern = '/:("[^"]*?"|\'[^\']*?\'|[^:|]*)/';
  658. // has arguments
  659. if(preg_match_all($pattern, $modifiers[2][$key], $arguments))
  660. {
  661. // loop arguments
  662. foreach($arguments[1] as $argument)
  663. {
  664. // string argument?
  665. if(in_array(substr($argument, 0, 1), array('\'', '"')))
  666. {
  667. // in compiled code: single quotes! (and escape single quotes in the content!)
  668. $argument = '\'' . str_replace('\'', '\\\'', substr($argument, 1, -1)) . '\'';
  669. // make sure that variables inside string arguments are correctly parsed
  670. $argument = preg_replace('/\[\$.*?\]/', '\' . \\0 .\'', $argument);
  671. }
  672. // add argument
  673. $PHP .= ', ' . $argument;
  674. }
  675. }
  676. }
  677. // add close tag
  678. $PHP .= ')';
  679. }
  680. }
  681. }
  682. /**
  683. * Variables may have other variables used as parameters in modifiers
  684. * so loop all currently known variables to replace them.
  685. * It does not matter that we do not yet know all variables, we only
  686. * need those inside this particular variable, and those will
  687. * certainly already be parsed because we parse our variables outwards.
  688. */
  689. // temporary variable which is a list of 'variables to check before parsing'
  690. $variables = array($variable);
  691. // loop all known template variables
  692. foreach($this->templateVariables as $key => $value)
  693. {
  694. // replace variables
  695. $PHP = str_replace('[$' . $key . ']', $value['content'], $PHP);
  696. // debug enabled
  697. if(SPOON_DEBUG)
  698. {
  699. // check if this variable is found
  700. if(strpos($match[0], '[$' . $key . ']') !== false)
  701. {
  702. // add variable name to list of 'variables to check before parsing'
  703. $variables = array_merge($variables, $value['variables']);
  704. }
  705. }
  706. }
  707. // PHP conversion for this template variable
  708. $this->templateVariables[$varKey]['content'] = $PHP;
  709. // debug enabled: variable not assigned = revert to template code
  710. if(SPOON_DEBUG)
  711. {
  712. // holds checks to see if this variable can be parsed (along with the variables that may be used inside it)
  713. $exists = array();
  714. // loop variables
  715. foreach((array) $variables as $variable)
  716. {
  717. // get array containing variable
  718. $array = preg_replace('/(\[\'[a-z_][a-z0-9_]*\'\])$/i', '', $variable);
  719. // get variable name
  720. preg_match('/\[\'([a-z_][a-z0-9_]*)\'\]$/i', $variable, $variable);
  721. $variable = $variable[1];
  722. // container array is index of higher array
  723. if(preg_match('/\[\'[a-z_][a-z0-9_]*\'\]/i', $array)) $exists[] = 'isset(' . $array . ')';
  724. $exists[] = 'array_key_exists(\'' . $variable . '\', (array) ' . $array . ')';
  725. }
  726. // save info for error fallback
  727. $this->templateVariables[$varKey]['if'] = implode(' && ', $exists);
  728. $this->templateVariables[$varKey]['variables'] = $variables;
  729. $this->templateVariables[$varKey]['template'] = $match[0];
  730. }
  731. }
  732. // replace in content
  733. $content = str_replace($match[0], '[$' . $varKey . ']', $content);
  734. }
  735. }
  736. }
  737. // break the loop, no matches were found
  738. else break;
  739. }
  740. return $content;
  741. }
  742. /**
  743. * Prepare iterations (recursively).
  744. * Every single iteration (even iterations that are the same) has to be unique: iterations that are
  745. * the same can be nested in each other and cycle tags need to know exactly which iteration is cycling.
  746. *
  747. * @return string The updated content, containing reworked (unique) iteration tags.
  748. * @param string $content The content that may contain the iteration tags.
  749. */
  750. protected function prepareIterations($content)
  751. {
  752. // fetch iterations - for iterations that are present more than one, only the last one will
  753. // be matched, previous ones will be matches later on another run through our while loop
  754. $pattern = '/(\{iteration:([a-z][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*)\})(?!.*?\{iteration:\\2\})(.*?)(\{\/iteration:\\2\})/is';
  755. // we want to keep parsing iterations until none can be found
  756. while(1)
  757. {
  758. // replace iteration names to ensure that they're unique
  759. $content = preg_replace_callback($pattern, array($this, 'prepareIterationsCallback'), $content, -1, $count);
  760. // break the loop, no matches were found
  761. if(!$count) break;
  762. }
  763. return $content;
  764. }
  765. /**
  766. * Prepare iterations: callback function.
  767. *
  768. * @return string The updated iteration, containing a reworked (unique) iteration tag.
  769. * @param string $match The regex-match for an iteration.
  770. */
  771. protected function prepareIterationsCallback($match)
  772. {
  773. // increment iterations counter
  774. $this->iterationsCounter++;
  775. // return the modified iteration name
  776. return '{iteration_' . $this->iterationsCounter . ':' . $match[2] . '}' . $match[4] . '{/iteration_' . $this->iterationsCounter . ':' . $match[2] . '}';
  777. }
  778. /**
  779. * Now loop the vars again, but this time parse them in the content we're actually working with.
  780. *
  781. * @return string The updated content, containing reworked fully parsed variables.
  782. * @param string $content The content that may contain the partially parsed variables.
  783. */
  784. protected function replaceVariables($content)
  785. {
  786. // keep finding those variables!
  787. while(1)
  788. {
  789. // counter to check if we have actually replaced something
  790. $replaced = 0;
  791. // loop all variables
  792. foreach($this->templateVariables as $key => $value)
  793. {
  794. // replace variables in the content
  795. if(SPOON_DEBUG) $content = str_replace('[$' . $key . ']', '<?php if(' . $value['if'] . ') { echo ' . $value['content'] . '; } else { ?>' . $value['template'] . '<?php } ?>', $content, $count);
  796. else $content = str_replace('[$' . $key . ']', '<?php echo ' . $value['content'] . '; ?>', $content, $count);
  797. // add amount of replacements to our counter
  798. $replaced += $count;
  799. }
  800. // break the loop, no matches were found
  801. if($replaced == 0) break;
  802. }
  803. return $content;
  804. }
  805. /**
  806. * Set the cache directory.
  807. *
  808. * @param string $path The location of the cache directory to store cached template blocks.
  809. */
  810. public function setCacheDirectory($path)
  811. {
  812. $this->cacheDirectory = (string) $path;
  813. }
  814. /**
  815. * Set the compile directory.
  816. *
  817. * @param string $path The location of the compile directory to store compiled templates in.
  818. */
  819. public function setCompileDirectory($path)
  820. {
  821. $this->compileDirectory = (string) $path;
  822. }
  823. /**
  824. * If enabled, recompiles a template even if it has already been compiled.
  825. *
  826. * @param bool[optional] $on Should this template be recompiled every time it's loaded.
  827. */
  828. public function setForceCompile($on = true)
  829. {
  830. $this->forceCompile = (bool) $on;
  831. }
  832. /**
  833. * Sets the forms.
  834. *
  835. * @param array $forms An array of forms that need to be included in this template.
  836. */
  837. public function setForms(array $forms)
  838. {
  839. $this->forms = $forms;
  840. }
  841. /**
  842. * Strips php code from the content.
  843. *
  844. * @return string The updated content, no longer containing php code.
  845. * @param string $content The content that may contain php code.
  846. */
  847. protected function stripCode($content)
  848. {
  849. return $content = preg_replace('/\<\?(php)?(.*)\?\>/si', '', $content);
  850. }
  851. /**
  852. * Strip comments from the output.
  853. *
  854. * @return string The updated content, no longer containing template comments.
  855. * @param string $content The content that may contain template comments.
  856. */
  857. protected function stripComments($content)
  858. {
  859. // we want to keep stripping comments until none can be found
  860. do
  861. {
  862. // strip comments from output
  863. $content = preg_replace('/\{\*(?!.*?\{\*).*?\*\}/s', '', $content, -1, $count);
  864. }
  865. while($count > 0);
  866. return $content;
  867. }
  868. }