template.class.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. <?php
  2. namespace Juju\Data;
  3. require_once(realpath(dirname(__DIR__).'/events.trait.php'));
  4. require_once('earray.class.php');
  5. use Juju\{Events, Data\EArray};
  6. use \Exception;
  7. class Template {
  8. use Events;
  9. private static $templates = [];
  10. public static $cachedir;
  11. public static $basedir = __DIR__;
  12. private static $regex = [
  13. 'match'=>'/\{([^#\/?_][^}\n]*?)\}/i',
  14. 'parentmatch'=>'/\{\.\.\/([^#\/?_][^}\n]*?)\}/i',
  15. 'each'=>'/\{#each ([^}]*)\}([\S\s]*)\{\/each \1\}/i',
  16. 'exist'=>'/\{#exist ([^}]*)\}([\S\s]*)\{\/exist \1\}/i',
  17. 'existelse'=>'/\{#exist ([^}]*)\}([\S\s]*)\{#else \1\}([\S\s]*)\{\/exist \1\}/i',
  18. 'ignore'=>'/\{#ignore\}([\S\s]*)\{\/ignore\}/i',
  19. 'ignored'=>'/\{#ignored (\d+?)\}/i',
  20. 'gettext'=>"/{_([^,}]+)(?:, ?([^},]+))*\}/i",
  21. 'gettext_string'=>'/^([\'"])(.+)\1$/i',
  22. 'echo'=>'/\{=([^}]+)\}/i',
  23. 'eval'=>'/\{\?([\W\w\S\s]+)\?\}/i',
  24. 'include'=>'/{#include ([^}]+)}/i'
  25. ];
  26. protected static $parsers;
  27. private $template;
  28. private $name;
  29. private $path;
  30. public function __construct(string $name, string $template, bool $is_file = false){
  31. if(is_null(static::$parsers)){
  32. static::$parsers = [
  33. 'ignore'=>function(&$output, &$ignored){
  34. $output = preg_replace_callback(static::$regex['ignore'], function($matches) use(&$ignored){
  35. $ignored[] = $matches[1];
  36. return '{#ignored '.(count($ignored) - 1).'}';
  37. }, $output);
  38. },
  39. 'include'=>function(&$output, &$ignored = null){
  40. while(preg_match(static::$regex['include'], $output)){
  41. $output = preg_replace_callback(static::$regex['include'], function($matches) use(&$ignored){
  42. $path = static::$basedir.'/'.$matches[1];
  43. if(file_exists($path)){
  44. $output = file_get_contents($path);
  45. if(!is_null($ignored)){
  46. static::$parsers['ignore']($output, $ignored);
  47. }
  48. return $output;
  49. }
  50. return '';
  51. }, $output);
  52. }
  53. },
  54. 'each'=>[
  55. function(&$output, $data){
  56. $output = preg_replace_callback(static::$regex['each'], function($matches) use($data){
  57. $output = '';
  58. if(isset($data[$matches[1]])){
  59. foreach($data[$matches[1]] as $item){
  60. $output = static::parse($matches[2], $item);
  61. }
  62. }
  63. return $output;
  64. }, $output);
  65. },
  66. function(&$output){
  67. $output = preg_replace_callback(static::$regex['each'], function($matches){
  68. $output = "<?php if(isset(\$data[".var_export($matches[1], true)."])): ";
  69. $output .= "foreach(\$data[".var_export($matches[1], true)."] as \$item): ";
  70. $output .= "\$parent[] = \$data; \$data = \$item; ?>";
  71. $output .= static::compile($matches[2]);
  72. $output .= "<?php \$data = array_pop(\$parent);";
  73. $output .= "endforeach;endif; ?>";
  74. return $output;
  75. }, $output);
  76. }
  77. ],
  78. 'existelse'=>[
  79. function(&$output, $data){
  80. $output = preg_replace_callback(static::$regex['existelse'], function($matches) use($data){
  81. if(isset($data[$matches[1]])){
  82. $output = static::parse($matches[2], $data);
  83. }else{
  84. $output = static::parse($matches[3], $data);
  85. }
  86. return $output;
  87. }, $output);
  88. },
  89. function(&$output){
  90. $output = preg_replace_callback(static::$regex['existelse'], function($matches){
  91. $output = "<?php if(isset(\$data[".var_export($matches[1], true)."])): ?>";
  92. $output .= static::compile($matches[2]);
  93. $output .= "<php else: ?>";
  94. $output .= static::compile($matches[3]);
  95. $output .= "<?php endif; ?>";
  96. return $output;
  97. }, $output);
  98. }
  99. ],
  100. 'exist'=>[
  101. function(&$output, $data){
  102. $output = preg_replace_callback(static::$regex['exist'], function($matches) use($data){
  103. if(isset($data[$matches[1]])){
  104. return static::parse($data[$matches[2]], $data);
  105. }
  106. return '';
  107. }, $output);
  108. },
  109. function(&$output){
  110. $output = preg_replace_callback(static::$regex['exist'], function($matches){
  111. $output = "<?php if(isset(\$data[".var_export($matches[1], true)."])): ?>";
  112. $output .= static::compile($matches[2]);
  113. $output .= "<?php endif; ?>";
  114. }, $output);
  115. }
  116. ],
  117. 'gettext'=>[
  118. function(&$output, $data){
  119. $output = preg_replace_callback(static::$regex['gettext'], function($matches) use($data){
  120. $args = array_map(function($item) use($data){
  121. if(preg_match(static::$regex['gettext_string'], $item)){
  122. return preg_replace(static::$regex['gettext_string'], '\2', $item);
  123. }else{
  124. return $data[$item] ?? '';
  125. }
  126. }, array_slice($matches, 1));
  127. return htmlentities(_(sprintf(...$args)));
  128. }, $output);
  129. },
  130. function(&$output){
  131. $output = preg_replace_callback(static::$regex['gettext'], function($matches){
  132. if(count($matches) > 2){
  133. $output = "<?=htmlentities(sprintf(_({$matches[1]})";
  134. foreach(array_slice($matches, 2) as $item){
  135. if(preg_match(static::$regex['gettext_string'], $item)){
  136. $output .= ", $item";
  137. }else{
  138. $output .= ", (\$data['{$item}'] ?? '')";
  139. }
  140. }
  141. }else{
  142. $output = "<?=htmlentities(_({$matches[1]}";
  143. }
  144. return "{$output})); ?>";
  145. }, $output);
  146. }
  147. ],
  148. 'echo'=>[
  149. function(&$output){
  150. $output = preg_replace_callback(static::$regex['echo'], function($matches){
  151. return eval("return {$matches[1]};");
  152. }, $output);
  153. },
  154. function(&$output){
  155. $output = preg_replace_callback(static::$regex['echo'], function($matches){
  156. return "<?= {$matches[1]}; ?>";
  157. }, $output);
  158. }
  159. ],
  160. 'eval'=>[
  161. function(&$output){
  162. $output = preg_replace_callback(static::$regex['eval'], function($matches){
  163. ob_start();
  164. eval($matches[1]);
  165. $output = ob_get_contents();
  166. ob_end_clean();
  167. return $output;
  168. }, $output);
  169. },
  170. function(&$output){
  171. $output = preg_replace_callback(static::$regex['eval'], function($matches){
  172. return "<?php {$matches[1]}; ?>";
  173. }, $output);
  174. }
  175. ],
  176. 'match'=>[
  177. function(&$output, $data){
  178. $output = preg_replace_callback(static::$regex['match'], function($matches) use($data){
  179. return htmlentities($data[$matches[1]] ?? '');
  180. }, $output);
  181. },
  182. function(&$output){
  183. $output = preg_replace_callback(static::$regex['match'], function($matches){
  184. return "<?=htmlentities(\$data[".var_export($matches[1], true)."] ?? ''); ?>";
  185. }, $output);
  186. }
  187. ],
  188. 'parentmatch'=>[
  189. function(&$output, $data){
  190. throw new \Exception("Not supported in non-compiled templates");
  191. },
  192. function(&$output){
  193. $output = preg_replace_callback(static::$regex['parentmatch'], function($matches){
  194. return "<?=htmlentities(\$parent[count(\$parent)-1][".var_export($matches[1], true)."] ?? ''); ?>";
  195. }, $output);
  196. }
  197. ],
  198. 'ignored'=>function(&$output, $ignored){
  199. $output = preg_replace_callback(static::$regex['ignored'], function($matches) use($ignored){
  200. return htmlentities($ignored[(int)$matches[1]] ?? '');
  201. }, $output);
  202. }
  203. ];
  204. }
  205. if(isset(static::$templates[$name])){
  206. throw new Exception("Template {$name} already exists");
  207. }
  208. if($is_file){
  209. $path = realpath($template);
  210. if(!file_exists($path)){
  211. throw new Exception("Template file {$template} doesn't exist");
  212. }
  213. $template = file_get_contents($path);
  214. }
  215. static::$parsers['include']($template);
  216. $this->template = $template;
  217. $this->name = $name;
  218. $this->path = static::$cachedir."/{$this->name}.".md5($this->template).'.php';
  219. static::$templates[$name] = $this;
  220. }
  221. public function run(array $data) : string{
  222. $data = EArray::from($data);
  223. if($this->fire('before', $data) === false){
  224. throw new Exception("Render on template {$this->name} cancelled. Before.");
  225. }
  226. if(!file_exists($this->path)){
  227. file_put_contents($this->path, static::compile($this->template));
  228. }
  229. try{
  230. $output = static::execute($this->path, $data);
  231. }catch(Exception $e){
  232. file_put_contents($this->path, static::compile($this->template));
  233. $output = static::execute($this->path, $data);
  234. }
  235. if($this->fire('after', $output) === false){
  236. throw new Exception("Render on template {$this->name} cancelled. After");
  237. }
  238. return (string)$output;
  239. }
  240. public static function from(string $name, array $data = []) : string{
  241. $template = static::$templates[$name] ?? null;
  242. if(is_null($template)){
  243. throw new Exception("Template {$name} does not exist");
  244. }
  245. return $template->run($data);
  246. }
  247. public static function compile(string $template) : string{
  248. $ignored = [];
  249. $output = $template;
  250. // Handle {#ignore code}
  251. static::$parsers['ignore']($output, $ignored);
  252. // Handle {#include path/to/file}
  253. static::$parsers['include']($output, $ignored);
  254. // Handle {#each name}{/each}
  255. static::$parsers['each'][1]($output);
  256. // Handle {#exist name}{#else}{/exist}
  257. static::$parsers['existelse'][1]($output);
  258. // Handle {#exist name}{/exist}
  259. static::$parsers['exist'][1]($output);
  260. // Handle {gettext}
  261. static::$parsers['gettext'][1]($output);
  262. // Handle {=expression}
  263. static::$parsers['echo'][1]($output);
  264. // Handle {? expression ?}
  265. static::$parsers['eval'][1]($output);
  266. // Handle {../name}
  267. static::$parsers['parentmatch'][1]($output);
  268. // Handle {name}
  269. static::$parsers['match'][1]($output);
  270. // Handle {#ignored i}
  271. static::$parsers['ignored']($output, $ignored);
  272. return $output;
  273. }
  274. public static function parse(string $template, $data) : string{
  275. $ignored = [];
  276. $output = $template;
  277. // Handle {#ignore code}
  278. static::$parsers['ignore']($output, $ignored);
  279. // Handle {#include path/to/file}
  280. static::$parsers['include']($output, $ignored);
  281. // Handle {#each name}{/each}
  282. static::$parsers['each'][0]($output, $data);
  283. // Handle {#exist name}{#else}{/exist}
  284. static::$parsers['existelse'][0]($output, $data);
  285. // Handle {#exist name}{/exist}
  286. static::$parsers['exist'][0]($output, $data);
  287. // Handle {gettext}
  288. static::$parsers['gettext'][0]($output, $data);
  289. // Handle {=expression}
  290. static::$parsers['echo'][0]($output);
  291. // Handle {? expression ?}
  292. static::$parsers['eval'][0]($output);
  293. // Handle {../name}
  294. static::$parsers['parentmatch'][1]($output, $data);
  295. // Handle {name}
  296. static::$parsers['match'][0]($output, $data);
  297. // Handle {#ignored i}
  298. static::$parsers['ignored']($output, $ignored);
  299. return $output;
  300. }
  301. public static function execute(string $path, $data) : string{
  302. ob_start();
  303. include($path);
  304. $output = ob_get_contents();
  305. ob_end_clean();
  306. return $output;
  307. }
  308. }
  309. ?>