template.class.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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. ];
  31. private static $regex = [
  32. 'match'=>'/\{([^#\/?_][^}\n]*?)\}/i',
  33. 'parentmatch'=>'/\{\.\.\/([^#\/?_][^}\n]*?)\}/i',
  34. 'each'=>'/\{#each ([^}]*)\}([\S\s]*)\{\/each \1\}/i',
  35. 'exist'=>'/\{#exist ([^}]*)\}([\S\s]*)\{\/exist \1\}/i',
  36. 'existelse'=>'/\{#exist ([^}]*)\}([\S\s]*)\{#else \1\}([\S\s]*)\{\/exist \1\}/i',
  37. 'ignore'=>'/\{#ignore\}([\S\s]*)\{\/ignore\}/i',
  38. 'ignored'=>'/\{#ignored (\d+?)\}/i',
  39. 'gettext'=>"/{_([^,}]+)(?:, ?([^},]+))*\}/i",
  40. 'gettext_string'=>'/^([\'"])(.+)\1$/i',
  41. 'echo'=>'/\{=([^}]+)\}/i',
  42. 'eval'=>'/\{\?([\W\w\S\s]+)\?\}/i',
  43. 'include'=>'/{#include ([^}]+)}/i',
  44. 'define'=>'/\{#define ([^}]*)\}([\S\s]*)\{\/define \1\}/i',
  45. 'widget'=>'/{#widget ([^}]+)}/i'
  46. ];
  47. protected static $parsers;
  48. private $template;
  49. private $name;
  50. private $path;
  51. public function __construct(string $name, string $template, bool $is_file = false){
  52. if(is_null(static::$parsers)){
  53. static::$parsers = [
  54. 'ignore'=>function(&$output, &$ignored){
  55. $output = preg_replace_callback(static::$regex['ignore'], function($matches) use(&$ignored){
  56. $ignored[] = $matches[1];
  57. return '{#ignored '.(count($ignored) - 1).'}';
  58. }, $output);
  59. },
  60. 'include'=>function(&$output, &$ignored = null){
  61. while(preg_match(static::$regex['include'], $output)){
  62. $output = preg_replace_callback(static::$regex['include'], function($matches) use(&$ignored){
  63. $path = static::$basedir.'/'.$matches[1];
  64. if(file_exists($path)){
  65. $output = file_get_contents($path);
  66. if(!is_null($ignored)){
  67. static::$parsers['ignore']($output, $ignored);
  68. }
  69. return $output;
  70. }
  71. return '';
  72. }, $output);
  73. }
  74. },
  75. 'define'=>function(&$output, &$widgets){
  76. while(preg_match(static::$regex['define'], $output)){
  77. $output = preg_replace_callback(static::$regex['define'], function($matches) use(&$widgets){
  78. $name = $matches[1];
  79. if(isset($widgets[$name])){
  80. throw new \Exception("Widget {$name} is already defined");
  81. }
  82. $widgets[$name] = $matches[2];
  83. return '';
  84. }, $output);
  85. }
  86. },
  87. 'widget'=>function(&$output, $widgets){
  88. while(preg_match(static::$regex['widget'], $output)){
  89. $output = preg_replace_callback(static::$regex['widget'], function($matches) use(&$widgets){
  90. $name = $matches[1];
  91. if(!isset($widgets[$name])){
  92. throw new \Exception("Widget {$name} is not defined");
  93. }
  94. return $widgets[$name];
  95. }, $output);
  96. }
  97. },
  98. 'each'=>function(&$output){
  99. $output = preg_replace_callback(static::$regex['each'], function($matches){
  100. $output = "<?php if(isset(\$data[".var_export($matches[1], true)."])): ";
  101. $output .= "foreach(\$data[".var_export($matches[1], true)."] as \$item): ";
  102. $output .= "\$parent[] = \$data; \$data = \$item; ?>";
  103. $output .= static::compile($matches[2]);
  104. $output .= "<?php \$data = array_pop(\$parent);";
  105. $output .= "endforeach;endif; ?>";
  106. return $output;
  107. }, $output);
  108. },
  109. 'existelse'=>function(&$output){
  110. $output = preg_replace_callback(static::$regex['existelse'], function($matches){
  111. $output = "<?php if(isset(\$data[".var_export($matches[1], true)."])): ?>";
  112. $output .= static::compile($matches[2]);
  113. $output .= "<?php else: ?>";
  114. $output .= static::compile($matches[3]);
  115. $output .= "<?php endif; ?>";
  116. return $output;
  117. }, $output);
  118. },
  119. 'exist'=>function(&$output){
  120. $output = preg_replace_callback(static::$regex['exist'], function($matches){
  121. $output = "<?php if(isset(\$data[".var_export($matches[1], true)."])): ?>";
  122. $output .= static::compile($matches[2]);
  123. $output .= "<?php endif; ?>";
  124. return $output;
  125. }, $output);
  126. },
  127. 'gettext'=>function(&$output){
  128. $output = preg_replace_callback(static::$regex['gettext'], function($matches){
  129. if(count($matches) > 2){
  130. $output = "<?=htmlentities(sprintf(_({$matches[1]})";
  131. foreach(array_slice($matches, 2) as $item){
  132. if(preg_match(static::$regex['gettext_string'], $item)){
  133. $output .= ", $item";
  134. }else{
  135. $output .= ", (\$data['{$item}'] ?? '')";
  136. }
  137. }
  138. }else{
  139. $output = "<?=htmlentities(_({$matches[1]}";
  140. }
  141. return "{$output})); ?>";
  142. }, $output);
  143. },
  144. 'echo'=>function(&$output){
  145. $output = preg_replace_callback(static::$regex['echo'], function($matches){
  146. return "<?= {$matches[1]}; ?>";
  147. }, $output);
  148. },
  149. 'eval'=>function(&$output){
  150. $output = preg_replace_callback(static::$regex['eval'], function($matches){
  151. return "<?php {$matches[1]}; ?>";
  152. }, $output);
  153. },
  154. 'match'=>function(&$output){
  155. $output = preg_replace_callback(static::$regex['match'], function($matches){
  156. return "<?=htmlentities(\$data[".var_export($matches[1], true)."] ?? ''); ?>";
  157. }, $output);
  158. },
  159. 'parentmatch'=>function(&$output){
  160. $output = preg_replace_callback(static::$regex['parentmatch'], function($matches){
  161. return "<?=htmlentities(\$parent[count(\$parent)-1][".var_export($matches[1], true)."] ?? ''); ?>";
  162. }, $output);
  163. },
  164. 'ignored'=>function(&$output, $ignored){
  165. $output = preg_replace_callback(static::$regex['ignored'], function($matches) use($ignored){
  166. return htmlentities($ignored[(int)$matches[1]] ?? '');
  167. }, $output);
  168. }
  169. ];
  170. }
  171. if(isset(static::$templates[$name])){
  172. throw new Exception("Template {$name} already exists");
  173. }
  174. if($is_file){
  175. $path = realpath($template);
  176. if(!file_exists($path)){
  177. throw new Exception("Template file {$template} doesn't exist");
  178. }
  179. $template = file_get_contents($path);
  180. }
  181. static::$parsers['include']($template);
  182. $widgets = [];
  183. static::$parsers['define']($template, $widgets);
  184. static::$parsers['widget']($template, $widgets);
  185. $this->template = $template;
  186. $this->name = $name;
  187. $this->path = static::$cachedir."/{$this->name}.".md5($this->template).'.php';
  188. static::$templates[$name] = $this;
  189. }
  190. public function run(array $data) : string{
  191. $data = EArray::from($data);
  192. if($this->fire('before', $data) === false){
  193. throw new Exception("Render on template {$this->name} cancelled. Before.");
  194. }
  195. if(!file_exists($this->path)){
  196. file_put_contents($this->path, static::compile($this->template));
  197. }
  198. try{
  199. $output = static::execute($this->path, $data);
  200. }catch(Exception $e){
  201. file_put_contents($this->path, static::compile($this->template));
  202. $output = static::execute($this->path, $data);
  203. }
  204. if(class_exists('tidy')){
  205. if(is_null(static::$tidy)){
  206. static::$tidy = new \tidy();
  207. }
  208. $tidy = static::$tidy;
  209. $tidy->parseString($output, static::$tidyConfig);
  210. if(!$tidy->cleanRepair()){
  211. throw new \Exception($tidy->errorBuffer);
  212. }
  213. $output = "{$tidy}";
  214. }
  215. if($this->fire('after', $output) === false){
  216. throw new Exception("Render on template {$this->name} cancelled. After");
  217. }
  218. return (string)$output;
  219. }
  220. public static function from(string $name, array $data = []) : string{
  221. $template = static::$templates[$name] ?? null;
  222. if(is_null($template)){
  223. throw new Exception("Template {$name} does not exist");
  224. }
  225. return $template->run($data);
  226. }
  227. public static function compile(string $template) : string{
  228. $ignored = [];
  229. $output = $template;
  230. // Handle {#ignore code}
  231. static::$parsers['ignore']($output, $ignored);
  232. // Handle {#include path/to/file}
  233. static::$parsers['include']($output, $ignored);
  234. // Handle {#each name}{/each}
  235. static::$parsers['each']($output);
  236. // Handle {#exist name}{#else}{/exist}
  237. static::$parsers['existelse']($output);
  238. // Handle {#exist name}{/exist}
  239. static::$parsers['exist']($output);
  240. // Handle {gettext}
  241. static::$parsers['gettext']($output);
  242. // Handle {=expression}
  243. static::$parsers['echo']($output);
  244. // Handle {? expression ?}
  245. static::$parsers['eval']($output);
  246. // Handle {../name}
  247. static::$parsers['parentmatch']($output);
  248. // Handle {name}
  249. static::$parsers['match']($output);
  250. // Handle {#ignored i}
  251. static::$parsers['ignored']($output, $ignored);
  252. return $output;
  253. }
  254. public static function parse(string $template, $data) : string{
  255. $id = md5($template);
  256. if(!isset(static::$templates[$id])){
  257. new Template($id, $template);
  258. }
  259. return static::$templates[$id]->run($data);
  260. }
  261. public static function execute(string $path, $data) : string{
  262. ob_start();
  263. include($path);
  264. $output = ob_get_contents();
  265. ob_end_clean();
  266. return $output;
  267. }
  268. }
  269. ?>