template.class.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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 $tidy;
  13. private static $tidyConfig = [
  14. 'indent'=>true,
  15. 'tab-size'=>4,
  16. 'wrap'=>0,
  17. 'wrap-asp'=>false,
  18. 'wrap-attributes'=>false,
  19. 'wrap-jste'=>false,
  20. 'wrap-php'=>false,
  21. 'wrap-script-literals'=>false,
  22. 'wrap-sections'=>false,
  23. 'char-encoding'=>'utf8',
  24. 'newline'=>'LF',
  25. 'tidy-mark'=>true,
  26. 'merge-divs'=>false,
  27. 'merge-spans'=>false,
  28. 'logical-emphasis'=>false,
  29. 'literal-attributes'=>true,
  30. 'drop-empty-elements'=>false
  31. ];
  32. private static $regex = [
  33. 'getmatch'=>'/\{!([^#\/?_][^}\n]*?)\}/i',
  34. 'getparentmatch'=>'/\{!\.\.\/([^#\/?_][^}\n]*?)\}/i',
  35. 'getrawmatch'=>'/\{!@([^#\/?_][^}\n]*?)\}/i',
  36. 'getrawparentmatch'=>'/\{!@\.\.\/([^#\/?_][^}\n]*?)\}/i',
  37. 'match'=>'/\{([^#\/?_][^}\n]*?)\}/i',
  38. 'parentmatch'=>'/\{\.\.\/([^#\/?_][^}\n]*?)\}/i',
  39. 'rawmatch'=>'/\{@([^#\/?_][^}\n]*?)\}/i',
  40. 'rawparentmatch'=>'/\{@\.\.\/([^#\/?_][^}\n]*?)\}/i',
  41. 'each'=>'/\{#each ([^}]*)\}([\S\s]*)\{\/each \1\}/i',
  42. 'exist'=>'/\{#exist ([^}]*)\}([\S\s]*)\{\/exist \1\}/iU',
  43. 'existelse'=>'/\{#exist ([^}]*)\}([\S\s]*)\{#else \1\}([\S\s]*)\{\/exist \1\}/iU',
  44. 'ignore'=>'/\{#ignore\}([\S\s]*)\{\/ignore\}/i',
  45. 'ignored'=>'/\{#ignored (\d+?)\}/i',
  46. 'gettext'=>"/{_([^,}]+)(?:, ?([^},]+))*\}/i",
  47. 'gettext_string'=>'/^([\'"])(.+)\1$/i',
  48. 'echo'=>'/\{=([^}]+)\}/i',
  49. 'eval'=>'/\{\?([\W\w\S\s]+)\?\}/i',
  50. 'include'=>'/{#include ([^}]+)}/i',
  51. 'define'=>'/\{#define ([^}]*)\}([\S\s]*)\{\/define \1\}/i',
  52. 'widget'=>'/{#widget ([^ }]+)(?: ((?:[^=}]+=[^}&]+)*))?}/i'
  53. ];
  54. protected static $parsers;
  55. private $template;
  56. private $name;
  57. private $path;
  58. public function __construct(string $name, string $template, bool $is_file = false){
  59. if(is_null(static::$parsers)){
  60. static::$parsers = [
  61. 'ignore'=>function(&$output, &$ignored){
  62. $output = preg_replace_callback(static::$regex['ignore'], function($matches) use(&$ignored){
  63. $ignored[] = $matches[1];
  64. return '{#ignored '.(count($ignored) - 1).'}';
  65. }, $output);
  66. },
  67. 'include'=>function(&$output, &$ignored = null){
  68. while(preg_match(static::$regex['include'], $output)){
  69. $output = preg_replace_callback(static::$regex['include'], function($matches) use(&$ignored){
  70. $path = static::$basedir.'/'.$matches[1];
  71. if(file_exists($path)){
  72. $output = file_get_contents($path);
  73. if(!is_null($ignored)){
  74. static::$parsers['ignore']($output, $ignored);
  75. }
  76. return $output;
  77. }
  78. return '';
  79. }, $output);
  80. }
  81. },
  82. 'define'=>function(&$output, &$widgets){
  83. while(preg_match(static::$regex['define'], $output)){
  84. $output = preg_replace_callback(static::$regex['define'], function($matches) use(&$widgets){
  85. $name = $matches[1];
  86. if(isset($widgets[$name])){
  87. throw new \Exception("Widget {$name} is already defined");
  88. }
  89. $widgets[$name] = $matches[2];
  90. return '';
  91. }, $output);
  92. }
  93. },
  94. 'widget'=>function(&$output, $widgets){
  95. while(preg_match(static::$regex['widget'], $output)){
  96. $output = preg_replace_callback(static::$regex['widget'], function($matches) use(&$widgets){
  97. $name = $matches[1];
  98. if(!isset($widgets[$name])){
  99. throw new \Exception("Widget {$name} is not defined");
  100. }
  101. if(count($matches) > 2){
  102. $args = [];
  103. foreach(explode('&', $matches[2]) as $chunk){
  104. $param = explode('=', $chunk);
  105. if($param){
  106. $args[urldecode($param[0])] = urldecode($param[1]);
  107. }
  108. }
  109. $widget = "<?php \$widget_parent[] = \$data; \$data = array_merge(json_decode(json_encode(\$data), true), json_decode(base64_decode(".var_export(base64_encode(json_encode($args)), true)."), true)); ?>";
  110. $widget .= static::compile($widgets[$name]);
  111. $widget .= "<?php \$data = array_pop(\$widget_parent); ?>";
  112. }else{
  113. $widget = $widgets[$name];
  114. }
  115. return $widget;
  116. }, $output);
  117. }
  118. },
  119. 'each'=>function(&$output){
  120. $output = preg_replace_callback(static::$regex['each'], function($matches){
  121. $output = "<?php if(isset(\$data[".var_export($matches[1], true)."])): ";
  122. $output .= "foreach(\$data[".var_export($matches[1], true)."] as \$item): ";
  123. $output .= "\$parent[] = \$data; \$data = \$item; ?>";
  124. $output .= static::compile($matches[2]);
  125. $output .= "<?php \$data = array_pop(\$parent);";
  126. $output .= "endforeach;endif; ?>";
  127. return $output;
  128. }, $output);
  129. },
  130. 'existelse'=>function(&$output){
  131. $output = preg_replace_callback(static::$regex['existelse'], function($matches){
  132. $output = "<?php if(isset(\$data[".var_export($matches[1], true)."])): ?>";
  133. $output .= static::compile($matches[2]);
  134. $output .= "<?php else: ?>";
  135. $output .= static::compile($matches[3]);
  136. $output .= "<?php endif; ?>";
  137. return $output;
  138. }, $output);
  139. },
  140. 'exist'=>function(&$output){
  141. $output = preg_replace_callback(static::$regex['exist'], function($matches){
  142. $output = "<?php if(isset(\$data[".var_export($matches[1], true)."])): ?>";
  143. $output .= static::compile($matches[2]);
  144. $output .= "<?php endif; ?>";
  145. return $output;
  146. }, $output);
  147. },
  148. 'gettext'=>function(&$output){
  149. $output = preg_replace_callback(static::$regex['gettext'], function($matches){
  150. if(count($matches) > 2){
  151. $output = "<?=htmlentities(sprintf(_({$matches[1]})";
  152. foreach(array_slice($matches, 2) as $item){
  153. if(preg_match(static::$regex['gettext_string'], $item)){
  154. $output .= ", $item";
  155. }else{
  156. $output .= ", (\$data['{$item}'] ?? '')";
  157. }
  158. }
  159. }else{
  160. $output = "<?=htmlentities(_({$matches[1]}";
  161. }
  162. return "{$output})); ?>";
  163. }, $output);
  164. },
  165. 'echo'=>function(&$output){
  166. $output = preg_replace_callback(static::$regex['echo'], function($matches){
  167. return "<?= {$matches[1]}; ?>";
  168. }, $output);
  169. },
  170. 'eval'=>function(&$output){
  171. $output = preg_replace_callback(static::$regex['eval'], function($matches){
  172. return "<?php {$matches[1]}; ?>";
  173. }, $output);
  174. },
  175. 'getrawmatch'=>function(&$output){
  176. $output = preg_replace_callback(static::$regex['getrawmatch'], function($matches){
  177. return "<?=(\$data[\$data[".var_export($matches[1], true)."] ?? ''] ?? '');?>";
  178. }, $output);
  179. },
  180. 'getrawparentmatch'=>function(&$output){
  181. $output = preg_replace_callback(static::$regex['getrawparentmatch'], function($matches){
  182. return "<?=(\$parent[count(\$parent)-1][\$data[".var_export($matches[1], true)."] ?? ''] ?? '');?>";
  183. }, $output);
  184. },
  185. 'getmatch'=>function(&$output){
  186. $output = preg_replace_callback(static::$regex['getmatch'], function($matches){
  187. return "<?=htmlentities(\$data[\$data[".var_export($matches[1], true)."] ?? ''] ?? '');?>";
  188. }, $output);
  189. },
  190. 'getparentmatch'=>function(&$output){
  191. $output = preg_replace_callback(static::$regex['getparentmatch'], function($matches){
  192. return "<?=htmlentities(\$parent[count(\$parent)-1][\$data[".var_export($matches[1], true)."] ?? ''] ?? '');?>";
  193. }, $output);
  194. },
  195. 'rawmatch'=>function(&$output){
  196. $output = preg_replace_callback(static::$regex['rawmatch'], function($matches){
  197. return "<?=(\$data[".var_export($matches[1], true)."] ?? '');?>";
  198. }, $output);
  199. },
  200. 'rawparentmatch'=>function(&$output){
  201. $output = preg_replace_callback(static::$regex['rawparentmatch'], function($matches){
  202. return "<?=(\$parent[count(\$parent)-1][".var_export($matches[1], true)."] ?? '');?>";
  203. }, $output);
  204. },
  205. 'match'=>function(&$output){
  206. $output = preg_replace_callback(static::$regex['match'], function($matches){
  207. return "<?=htmlentities(\$data[".var_export($matches[1], true)."] ?? '');?>";
  208. }, $output);
  209. },
  210. 'parentmatch'=>function(&$output){
  211. $output = preg_replace_callback(static::$regex['parentmatch'], function($matches){
  212. return "<?=htmlentities(\$parent[count(\$parent)-1][".var_export($matches[1], true)."] ?? '');?>";
  213. }, $output);
  214. },
  215. 'ignored'=>function(&$output, $ignored){
  216. $output = preg_replace_callback(static::$regex['ignored'], function($matches) use($ignored){
  217. return htmlentities($ignored[(int)$matches[1]] ?? '');
  218. }, $output);
  219. }
  220. ];
  221. }
  222. if(isset(static::$templates[$name])){
  223. throw new Exception("Template {$name} already exists");
  224. }
  225. if($is_file){
  226. $path = realpath($template);
  227. if(!file_exists($path)){
  228. throw new Exception("Template file {$template} doesn't exist");
  229. }
  230. $template = file_get_contents($path);
  231. }
  232. static::$parsers['include']($template);
  233. $widgets = [];
  234. static::$parsers['define']($template, $widgets);
  235. static::$parsers['widget']($template, $widgets);
  236. $this->template = $template;
  237. $this->name = $name;
  238. $this->path = static::$cachedir."/{$this->name}.".md5($this->template).'.php';
  239. static::$templates[$name] = $this;
  240. }
  241. public function __get(string $name){
  242. switch($name){
  243. case 'name':case 'template':case 'path':
  244. return $this->$name;
  245. break;
  246. default:
  247. throw new \Exception("Property {$name} doesn't exist");
  248. }
  249. }
  250. public function to_file(){
  251. file_put_contents($this->path, static::compile($this->template));
  252. }
  253. public function run(array $data) : string{
  254. $data = EArray::from($data);
  255. if($this->fire('before', $data) === false){
  256. throw new Exception("Render on template {$this->name} cancelled. Before.");
  257. }
  258. if(!file_exists($this->path)){
  259. $this->to_file();
  260. }
  261. try{
  262. $output = static::execute($this->path, $data);
  263. }catch(Exception $e){
  264. $this->to_file();
  265. $output = static::execute($this->path, $data);
  266. }
  267. if(class_exists('tidy')){
  268. if(is_null(static::$tidy)){
  269. static::$tidy = new \tidy();
  270. }
  271. $tidy = static::$tidy;
  272. $tidy->parseString($output, static::$tidyConfig);
  273. if(!$tidy->cleanRepair()){
  274. throw new \Exception($tidy->errorBuffer);
  275. }
  276. $output = "{$tidy}";
  277. }
  278. if($this->fire('after', $output) === false){
  279. throw new Exception("Render on template {$this->name} cancelled. After");
  280. }
  281. return (string)$output;
  282. }
  283. public static function from(string $name, array $data = []) : string{
  284. $template = static::get($name);
  285. if(is_null($template)){
  286. throw new Exception("Template {$name} does not exist");
  287. }
  288. return $template->run($data);
  289. }
  290. public static function get(string $name){
  291. return static::$templates[$name] ?? null;
  292. }
  293. public static function compile(string $template) : string{
  294. $ignored = [];
  295. $output = $template;
  296. // Handle {#ignore code}
  297. static::$parsers['ignore']($output, $ignored);
  298. // Handle {#include path/to/file}
  299. static::$parsers['include']($output, $ignored);
  300. // Handle {#each name}{/each}
  301. static::$parsers['each']($output);
  302. // Handle {#exist name}{#else}{/exist}
  303. static::$parsers['existelse']($output);
  304. // Handle {#exist name}{/exist}
  305. static::$parsers['exist']($output);
  306. // Handle {gettext}
  307. static::$parsers['gettext']($output);
  308. // Handle {=expression}
  309. static::$parsers['echo']($output);
  310. // Handle {? expression ?}
  311. static::$parsers['eval']($output);
  312. // Handle {!@../name}
  313. static::$parsers['getrawparentmatch']($output);
  314. // Handle {!@name}
  315. static::$parsers['getrawmatch']($output);
  316. // Handle {!../name}
  317. static::$parsers['getparentmatch']($output);
  318. // Handle {!name}
  319. static::$parsers['getmatch']($output);
  320. // Handle {@../name}
  321. static::$parsers['rawparentmatch']($output);
  322. // Handle {@name}
  323. static::$parsers['rawmatch']($output);
  324. // Handle {../name}
  325. static::$parsers['parentmatch']($output);
  326. // Handle {name}
  327. static::$parsers['match']($output);
  328. // Handle {#ignored i}
  329. static::$parsers['ignored']($output, $ignored);
  330. return $output;
  331. }
  332. public static function parse(string $template, $data) : string{
  333. $id = md5($template);
  334. if(!isset(static::$templates[$id])){
  335. new Template($id, $template);
  336. }
  337. return static::$templates[$id]->run($data);
  338. }
  339. public static function execute(string $path, $data) : string{
  340. ob_start();
  341. include($path);
  342. $output = ob_get_contents();
  343. ob_end_clean();
  344. return $output;
  345. }
  346. public static function templates(){
  347. return static::$templates;
  348. }
  349. }
  350. ?>