<?php
// last edit: 02.11.2018

/**
* Данный шаблонизатор был разработан для фреймворка, но не является обязательным в использовании, 
* его можно заменить в файле View.class.php, на любой другой, например Twig или Smarty.
*
* Шаблонизатор по большей части напоминает Blade, однако имеет свои особенности и лишен фильтров, 
* в замен им позволяет использовать подгруженные функции из любых модулей, а так же имеет ряд своих ФИЧ.
*
* Так же данный шаблонизатор в данный момент не имеет удобного вывода отладочной информации

Краткое содержание функционала:

GlobalVars  - Класс для хранения и использования псевдоглаббального окружения, с видимостью "только в шаблонах", этот класс используется автоматически после запуска скомпилированного шаблона
	::set($name, $value)  - устанавливает новое значение переменной (в том числе и переданной в шаблон)
	::get($name)          - позволяет получить значение глобальной переменной (в том числе и переданной в шаблон)
	::call($name)         - вставляет контент указанного блока

TemplateEngine  - Класс самого движка шаблонизатора, все настройки и запуск механизмов шаблонизатора осуществляется через него.
	::$path               - путь к каталогу, в котором находятся шаблоны в формате "<имя>.template.php"
	::$pathtmp            - путь к каталогу, в котором будут создаваться временные скомпилированные файлы-страницы
	::$methods            - массив тегов/методом шаблонизатора, которые используются в шаблоне. При желании добавить свои теги, следует добавлять их в этот массив по принципу "TemplateEngine::$methods['Имя тега'] = function([<параметры, если необходимы>]){ return "текст/html"; }"
	::$symMethodDelemiter - символ/набор символов, которое сообщает шаблонизатору о начале команды/тега, отработка которого описана в $methods
	::$symCommentStart    - начало блока комментариев, которые будут удалены из шаблона при компиляции
	::$symCommentEnd      - конец блока коментариев
	::$debug              - если установить в TRUE, то скомпилированные страницы не будут удаляться после отработки шаблонизатора

	// Стоит заметить, что при критической ошибке в шаблоне, страница так же не будет удалена. Такие страницы следует удалять вручную.
	// Конец команды/тега шаблонизатора является закрывающая скобка, пробел, или конец строки.
	// Вставка кода для вывода в страницу осуществляется по маске двойных фигурных скобок:  {{ 'текст который надо вставить в шаблон' }}

	::view(string $name, array $vars = []) 
		- метод загружает шаблон и связанные с ним шаблоны в память, закидывает переданный массив переменных, и компилирует в страницу php, затем запускает ее на выполнение, и есле нет ошибок и не включен режим дебага, удаляет эту страницу.
	
	::init()
		- запускает инициализацию шаблонизатора

	


	::nophp($str)
		- удаляет комбинации "<?", "?>", делая их обычными "<", ">". Используется автоматически при вставке фигурных скобок в шаблоне
	
	::addText($str)
		- добавляет текст к компиляции страницы, в страницу или текущий блок-конструкцию, или функцию-шаблон.

	::getBlocksCode()
		- Формирует код всех описанные в шаблонах блоков. Используется по завершению компиляции.

	::getTemplateCode()
		- Формирует код всех описанные в функций-шаблонов. Используется по завершению компиляции.

	::getPageCode()
		- Формирует код шаблона. Используется по завершению компиляции.

	::file_include($name)
		- загружает в текущее место шаблона другой шаблон. (примичание: не рекомендуется загружать в блок шаблон, содержащий описание других блоков)

	
Базовые теги/команды:

	@include(<template>)
		- загружает в текущее место другой шаблон

	@if(<compare>) 
	[@elseif(compare)] 
	[@else] 
	@end
		- условный блок, позволяет делать ветвление шаблона, и отображение части шаблона по условию.

	@while(<compare>) 
	@end
		- блок условного цикла, будет повторяться пока условие верно.

	@each(<list/array> [as <var>])
	[@else]
	@end
		- блок перебора массива, если массив не пустой, то повториться для каждого элемента массива
		- если же массив пустой, то будет вставлен блок из @else ветки, если таковая присутствует.
			
	@for([<var>=]<min>..<max>[,<step>])
	@end
		- блок итератора, будет повторен указанное число раз, если указать имя переменной, то в ней будет храниться текущий шаг. можно указывать значения как в большую, так и в меньшую сторону.

	@extends(<template>)
		- указывает шаблон-предка. Данную комманду рекомендуется использовать в самом верху шаблона (в первой строке).
	
	@block(<name>[,<default>])
	[@end]
		- описывает блок шаблона, который можно будет где-то вставить с помощью комманды @content. Данный блок заменяет содержимое предка, на свое.
		- если указано значение по умолчанию, то предпологается, что это значение и есть содержимое блока. В таком случае комманду @end стоит опустить.
	
	@section alias @block
		
	@blockadd(<name>[,<default>])
	[@end]
		- аналогично комманде @block, за исключением того, что не заменяет содержимое предка на свое, а дополняет его содержимое своим

	@sectionadd alias @blockadd
			
	@content(<nameblock>[,<default>])
		- Вставляет описанный блок шаблона, в текущее место
		- Если указанное имя блока не существует, то будет выведено указанное значение поумолчанию.

	@yield alias @content
			
	@set(<var>)
	@end
		- Позмоляет сохранить сформированный текст блока в переменную, для ее дольнейшего вывода в шаблоне. Данную команду можно использовать например для формирования списка, который надо вставить односременно в несколько позиций формы в шаблоне.
			
	@php(<php_code>)
		- позволяет использовать PHP-код как есть. По возможности следует откозаться от использования данной комманды, и использовать ее только в том случае, если нет другой возможности сделать так, как вам надо. Например изменить/сохранить в переменной промежуточное значение.
		
	@template(<name>)
	@end
		- функция-шаблон. Данная комманда была создана для возможности реализации рекурсивного шаблона, например для вывода дреевидного меню.
		- внутри данного блоака формируется локальная переменная $params, которая является массивом переданных в @call параметров, после имени функции-шаблона.
			
	@call(<nametemplate>[,<params>,...])
		- запускает выполнение функции-шаблона, передавая в нее необходимые значения.


	примечание: если вы не знаете, что такое рекурсия, то скорее всего вам не понадобится использование функции-шаблона.


По мимо этого, в данном файле определены функции, которые являются аналогом фильтров других шаблонизаторов:
	e($str)
		- экранирует весь html-код, что бы выводить его на страницу.

	h($str)
		- обратная операция функции e


Приммер добавления собственной комманды/тега в файле init.php
	TemplateEngine::$methods['ifguest'] = function(){ return TemplateEngine::_if('!users::isLogin()');};

	// в шаблоне это будет выглядеть например так:

	@ifguest
	...
	@else
	...
	@end

Какие наименования стандартных комманд можно использовать внутри самописных:

	TemplateEngine::file_include(string $template)
	TemplateEngine::_if(string $compare)
	TemplateEngine::_elseif(string $compare)
	TemplateEngine::_else()
	TemplateEngine::_while(string $compare)
	TemplateEngine::_each(string $foreach),
	TemplateEngine::_end()
	TemplateEngine::_for(string $for[, int/float $step=1]),
	TemplateEngine::_extends(string $template)
	TemplateEngine::_block(string $name [, string $default=null, bool $add=false]),
	TemplateEngine::_blockadd(string $name [, string $default=null])
	TemplateEngine::_content(string $nameblock [, string $default=null])
	TemplateEngine::_set(string $var)
	TemplateEngine::_php(string $phpcode)
	TemplateEngine::_template(string $name)
	TemplateEngine::_call(string $nametemplate [, mixed <param>,...])


Пример применения рекурсивного шаблона:
	@call(menu, $menus, $menu_active)

	@template(menu)
		@call(menuview, $params[0]->list, $params[0]->active_id)
		@if($params[0]->submenu)
			@call(menu, $params[0]->submenu)
		@end
	@end

	@template(menuview)
		<ul class=menu>
			@each($params[0] as $item)
			<li class='menu-item@if( $params[1] == $item->id ) -active@end()'><a href="{{ $item->url }}">{{ $item->name }}</a>
			@end
		</ul>
	@end


Примечание: имена блоков шаблона, шаблонов и функций-шаблонов, следует указывать без ковычек и пробелов, например:
	@extends(admin/groups)
	@include(admin/groups)
	@template(menu)
	@call(menu)
	@block(head)
	@content(head,'')

*/



class GlobalVars{
	static $_ = []; // VARS
	static $c = []; // contents
	static $f = []; // functions

	static function set($name,$value){
		static::$_[$name] = $value;
	}

	static function get($name){
		return static::$_[$name];
	}

	static function call($name){
		static::$c();
	}
}


class TemplateEngine {
	static public 
		$path = '.', 
		$pathtmp = '.', 
		$methods = [],
		$symMethodDelemiter = '@',
		$symCommentStart = '<!--',
		$debug = false,
		$symCommentEnd = '-->';

	static private 
		$extends = [],
		$blocks = [],
		$templates = [],
		$blockname = false,
		$templatename = false,
		$pageStr = [],
		$opers = [];

	static function view(string $name, array $vars = []){
		$pref = self::compile($name, $vars);
		return self::load($name, $vars, $pref);
	}

	static function init(){
		static::$methods = [
			'include' => 'TemplateEngine::file_include',
			'if' => 'TemplateEngine::_if',
			'elseif' => 'TemplateEngine::_elseif',
			'else' => 'TemplateEngine::_else',
			'while' => 'TemplateEngine::_while',
			'each' => 'TemplateEngine::_each',
			'end' => 'TemplateEngine::_end',
			'for' => 'TemplateEngine::_for',
			'extends' => 'TemplateEngine::_extends',
			'block' => 'TemplateEngine::_block',
			'section' => 'TemplateEngine::_block',
			'blockadd' => 'TemplateEngine::_blockadd',
			'sectionadd' => 'TemplateEngine::_blockadd',
			'content' => 'TemplateEngine::_content',
			'yield' => 'TemplateEngine::_content',
			'set' => 'TemplateEngine::_set',
			'php' => 'TemplateEngine::_php',
			'template' => 'TemplateEngine::_template',
			'call' => 'TemplateEngine::_call'
		];
	}

	static function nophp($str){
		if(is_string($str))
			return strtr($str,['<?'=>'<','?>'=>'>']);
		else return $str;
	}

	static function addText($str){
		if(static::$blockname)
			static::$blocks[static::$blockname] .= $str;
		elseif(static::$templatename)
			static::$templates[static::$templatename] .= $str;
		else static::$pageStr[] = $str;
	}

	static function getBlocksCode(){
		$result = [];
		static::$blocks = array_reverse(static::$blocks);
		foreach(static::$blocks as $name => $block)
			$result[] = "GlobalVars::\$c['$name'] = (function(){extract(GlobalVars::\$_,EXTR_REFS);ob_start(); ?>$block<?php return ob_get_clean();})();";
		return "<?php ".implode("\n", $result).'?>';
	}

	static function getTemplateCode(){
		$result = [];
		foreach(static::$templates as $name => $template)
			$result[] = "GlobalVars::\$f['$name'] = function(...\$params){extract(GlobalVars::\$_,EXTR_REFS);ob_start(); ?>$template<?php return ob_get_clean();};";
		return "<?php ".implode("\n", $result).'?>';
	}

	static function getPageCode(){
		return trim(implode('', static::$pageStr));
	}


	/*****************************************************************************/
	static function _php($line){
		return "<?php $line;?>";
	}

	static function _set($var){
		static::$opers[] = 'set';
		return "<?php $var = (function(){ ob_start(); ?>";
	}

	static function _extends($name){
		end(static::$extends);
		$key = key(static::$extends);
		if(static::$extends[$key])
			return '';

		static::$extends[$key] = true;
		static::file_include($name);
		return '';
	}

	static function _template($name){
		static::$opers[] = 'template';
		static::$templatename = $name;
		static::$templates[$name] = '';
		return '';
	}

	static function _block($name,$content=null, $add = false){
		static::$opers[] = 'block';
		$str = '';
		if(!is_null($content))
			try{
				eval('$str = '.trim($content).';');
			}catch(Exception $ex){
				$str = trim($content);
			}

		static::$blockname = $name;
		if(!isset(static::$blocks[$name]) || !$add)
			static::$blocks[$name] = $str;
		else static::$blocks[$name] .= $str;
		if(!is_null($content)) static::$blockname = false;

		return '';
	}

	static function _blockadd($name,$content=null){
		static::_block($name,$content, true);
		return '';
	}

	static function _end(){
		$ops = array_pop(static::$opers);
		switch ($ops) {
			case 'each':
				return '<?php endforeach;endif; ?>';
				break;

			case 'if': case 'else':
				return '<?php endif; ?>';
				break;

			case 'while':
				return '<?php endwhile; ?>';
				break;

			case 'for':
				return '<?php endfor; ?>';
				break;

			case 'block':
				static::$blockname = false;
				return '';
				break;

			case 'template':
				static::$templatename = false;
				return '';
				break;

			case 'set':
				return '<?php return ob_get_clean();})();?>';
				break;

			default:
				return '';
				break;
		}
		
	}

	static function _content($name,$def = null){
		if(is_null($def))
			return "<?php echo GlobalVars::\$c['$name']; ?>";
		else 
			return "<?php echo isset(GlobalVars::\$c['$name']) ? GlobalVars::\$c['$name'] : $def; ?>";
	}

	static function _call($name,...$args){
		$str = join(',',$args);
		return "<?php echo GlobalVars::\$f['$name']($str); ?>";
	}

	static function _else(){
		$ops = array_pop(static::$opers);
		if($ops == 'each'){
			$else = "<?php endforeach;else: ?>";
		} else $else = "<?php else: ?>";
		static::$opers[] = 'else';
		return $else;
	}

	static function _while($cmp){
		static::$opers[] = 'while';
		return "<?php while($cmp): ?>";
	}

	static function _each($mov){
		$i = strpos($mov, 'as');
		if($i === false){
			$var = trim($mov);
			$as = '$'.uniqid('v');
		}else {
			$var = trim(substr($mov,0,$i));
			$as = trim(substr($mov, $i+2));
		}
		static::$opers[] = 'each';

		return "<?php if(count($var)): foreach($var as $as): ?>";
	}

	static function _for($mov,$step=1){
		static::$opers[] = 'for';
		$i = strpos($mov,'=');
		if($i === false)
			$var = '$'.uniqid('i');
		else {
			$var = trim(substr($mov,0,$i));
			$mov = substr($mov, $i+1);
		}

		$i = strpos($mov,'..');
		if($i === false){
			$min = 1;
			$max = 1;
		}else {
			$min = trim(substr($mov, 0, $i));
			$max = substr($cmp, $i+2);
			if(is_numeric($min))
				$min = +$min;
			if(is_numeric($max))
				$max = +$max;
		}


		$plus = (is_numeric($min) && is_numeric($max)) ? $min < $max : true;

		if($step == 1)
			$stepstr = $plus ? "$var++" : "$var--";
		else 
			$stepstr = $plus ? "$var += $step" : "$var -= $step";

		$cmp = $plus ? '<=' : '>=';

		return "<?php for($var = $min; $var $cmp $max; $stepstr): ?>";
	}

	static function _if($cmp){
		static::$opers[] = 'if';
		return "<?php if($cmp): ?>";
	}

	static function _elseif($cmp){
		$ops = array_pop(static::$opers);
		if($ops == 'each'){
			$else = "<?php endforeach; elseif($cmp): ?>";
		} else $else = "<?php elseif($cmp): ?>";
		static::$opers[] = 'if';
		return $else;
	}

	/*****************************************************************************/

	static function file_include(string $name){
		static::$extends[$name] = false;
		$path = static::$path . "/$name.";

		if(file_exists($path.'php'))
			return file_get_contents($path.'php');
		if(file_exists($path.'html'))
			return file_get_contents($path.'html');


		$file = file_get_contents($path.'template.php');


		if(strpos($file, static::$symCommentStart) !== false){
			$comments = explode(static::$symCommentStart, $file);
			$end = static::$symCommentEnd;
			$comments = array_map(function($str)use($end){
				$ps = strpos($str,$end);
				return $ps === false ? $str : substr($str, $ps+strlen($end));
			}, $comments);
			$file = implode('', $comments);
		}

		$char = static::$symMethodDelemiter;

		$file = strtr($file, ['{{'=>'<?php echo TemplateEngine::nophp(', '}}' => ');?>',$char.$char=>'&#64;']);


		$file_methods = explode(static::$symMethodDelemiter, $file);
		$result = [];
		
		$start = array_shift($file_methods);
		if($start != '')
			static::addText($start);

		foreach ($file_methods as $str) {
			$strRes = static::line_compile($str);

			if(!is_array($strRes))
				static::addText($strRes);
			else {
				$func = array_shift($strRes[0]);

				
				if(isset(static::$methods[$func])){
					$callback = static::$methods[$func];
					$is_str = is_string($callback);


					if($is_str && strpos($callback, '<?php')===0)
						static::addText($callback);
					elseif(!isset($callback))
						static::addText( $callback(...$strRes[0]) );
					elseif($is_str && strpos($callback, '::')){
						$arr2 = explode('::', $callback);
						static::addText($arr2[0]::{$arr2[1]}(...$strRes[0]));
					}elseif(is_function($callback))
						static::addText($callback(...$strRes[0]));
					else static::addText($callback);

					static::addText($strRes[1]);
				}
			}
		}


		$temp = array_pop(static::$extends);
		return '';
	}

	/*****************************************************************************/

	static function _sq($str,$ps,&$result){
		$str_act = false;
		$str_sym = '';
		$param_start = $ps;

		$scbd = 0;

		for($i = $ps,$l = strlen($str); $i < $l; $i++){
			switch ($str{$i}) {
				case "'": case '"':
					if($str_act && $str_sym == $str{$i} && ($str{$i-1} != '\\' || $str{$i-1} == '\\'))
						$str_act = false;
					elseif(!$str_act){
						$str_sym = $str{$i};
						$str_act = true;
					}
					break;

				case '(': 
					if(!$str_act)
						$scbd++;
					break;

				case ')': 
					if(!$str_act){
						$scbd--;
						if($scbd < 0){
							$result[] = trim(substr($str, $param_start, $i - $param_start));
							return $i+1;
						}
					}
					break;

				case ',':
					if(!$str_act && $scbd == 0){
						$result[] = trim(substr($str, $param_start, $i - $param_start));
						$param_start = $i+1;
					}
					break;
			}
		}

		$result[] = trim(substr($str, $param_start));
		return strlen($str);
	}

	static function line_compile($str){
		if(trim($str) != '' && !in_array($str{0}, [' ',"\n","\r","\t"])){
			$ps = strpos($str, '(');
			$ps2 = strpos($str,strpbrk($str, " \n\r\t"));
			if($ps && ($ps2 === false || $ps < $ps2)){
				$params = [trim(substr($str, 0, $ps))];
				$ps = static::_sq($str,$ps+1,$params);
				$str = substr($str, $ps);
			} else {
				if($ps2){
					$params = [substr($str,0, $ps2)];
					$str = substr($str, $ps2);
				}else{ 
					$params = [$str];
					$str = '';
				}
			}
			return [$params, $str];

		} else return static::$symMethodDelemiter.$str;
	}

	static function compile(string $name,array $vars){
		static::$extends = [];
		static::$blocks = [];
		static::$blockname = '';
		static::$pageStr = [];
		GlobalVars::$_ = $vars;


		static::file_include($name);
		while(count(static::$opers) > 0)
			static::_end();

		$content = static::getTemplateCode().static::getBlocksCode().static::getPageCode();

		$structure = static::$pathtmp . '/' . dirname($name);
		if(!file_exists($structure))
			mkdir($structure, 0, true);

		$pref = uniqid();

		file_put_contents(static::$pathtmp . '/' . $name . $pref . '.php', $content);
		return $pref;
	}

	static function load(string $name, array $vars, string $pref){

		$this_file = static::$pathtmp . '/' . $name . $pref . '.php';
		if(file_exists($this_file)){
			if(count($vars)){
				foreach ($vars as &$value)
					$value = static::nophp($value);
				GlobalVars::$_ = $vars;
				extract($vars);
			}
			$file = include $this_file;

			if(!static::$debug)
				unlink($this_file);
			return $file;
		}

		return '';
	}

}


function h($str){
	//return html_entity_decode($str, ENT_QUOTES, 'UTF-8');
	return htmlspecialchars_decode($str,ENT_HTML5);
}

function e($str){
	return htmlspecialchars($str,ENT_HTML5);
}