Доброго здравия хабражителям!
Когда я заглянул в исходники MODx Evolution, меня едва ли не хватил удар. Рефакторить, рефакторить и рефакторить, как, наверное, сказал бы Ильич. По сему меня хватило едва ли на пару недель рефакторинга, после чего я забросил это дело, ибо времени откровенно не было. Но разговор пойдёт не об этом.
Система шаблонизации MODx на мой взгляд — одна из самых лучших. Особенно хорошо постарались разработчики в MODx Revolution. Всё логично, расширяемо, гибко и прям-таки пасторально. Можно сказать, синтаксис шаблонизации MODx — это почти что отдельный язык разметки. Именно такое вот восхищение стало причиной, по которой я стал использовать эту методику в других проектах. И для того, чтобы не заколачивать микроскопом гвозди, то есть не ставить для лендингов MODx, но иметь возможность использовать эту шаблонизацию, я написал отдельный класс шаблонизатора. И даже дал название — QuadBraces.
Однако, первое, о чём хочу предупредить — это то, что привычные MODx-ерам чанки, шаблоны, сниппеты и расширения в парсере хранятся не в БД, а в файлах. Путь к файлу с данными шаблона определяется путём определения константы PARSER_TPL_PATH.
Возможности
- Рекурсивная обработка текстовых данных;
- Используемые типы элементов: чанки, константы, установки, плейсхолдеры, отладка, сниппеты;
- Возможность определения т.н. темплейт-паков (в папке шаблонов парсера), что позволяет разрабатывать, устанавливать и использовать готовые пакеты шаблонов (как в Неназываемом-Мною-Обычно-Движке);
- Возможность структурирования шаблонного контента. Точка в алиасе чанка / шаблона / сниппета означает фактически разделитель директории;
- Ошибки генерируют исключения, что позволяет легче отлаживать код;
- Установка массива плейсхолдеров и «настроек» подразумевает дополнение существующих данных;
- Готовые алгоритмы аксессоров;
- В отличие от MODx глубина вложенности сниппетов ограничена только максимальным уровнем обработки парсера.
В сущности данный класс представляет собой рекурсивный обработчик текста на регулярках. Данные для плейсхолдеров и «настроек» определяются конечным разработчиком. Точно также и со сниппетами — это всё уже на совести того, кто будет использовать этот класс. В смысле сами пишите =)
Ниже исходник класса:
<?php if (!defined("PARSER_TPL_PATH")) { $_ = realpath($_SERVER['DOCUMENT_ROOT']).DIRECTORY_SEPARATOR; $_ = $_.implode(DIRECTORY_SEPARATOR,array('content','tpl')).DIRECTORY_SEPARATOR; define("PARSER_TPL_PATH",$_); } if (!defined("PARSER_STARTTIME")) define("PARSER_STARTTIME",microtime(true)); if (!defined("PARSER_STARTMEM")) define("PARSER_STARTMEM",memory_get_usage()); /** * Class quadBracesParser * @property string $template * @property-read string $templateName * @property string $templatePack * @property array $data * @property array $settings */ class quadBracesParser { protected static $_maxLevel = 32; protected static $_tags = null; protected $_debugTrace = array(); protected $_template = ''; protected $_templateName = ''; protected $_templatePack = ''; protected $_data = array(); protected $_settings = array(); protected $_arguments = array(); function __construct() { self::initTags(); $this->_debugTrace['starttime'] = microtime(true); } function __get($n) { if (method_exists($this,"get_$n")) { $f = "get_$n"; return $this->$f(); } elseif (property_exists($this,"_$n")) { $f = "_$n"; return $this->$f; } elseif (method_exists($this,"set_$n")) { $e = 'is write only'; } else { $e = property_exists($this,$n) ? 'is protected' : 'does not exists'; } throw new Exception("property $n $e"); } function __set($n,$v) { if (method_exists($this,"set_$n")) { $f = "set_$n"; return $this->$f($v); } elseif (method_exists($this,"get_$n")) { $e = 'is read only'; } else { $e = property_exists($this,$n) ? 'is protected' : 'does not exists'; } throw new Exception("property $n $e"); } function __toString() { return $this->parse(); } protected function set_template($name) { $content = '[*content*]'; if (!empty($name)) { $this->_templateName = ''; if ($fn = $this->search('template',$name)) { $content = @file_get_contents($fn); $this->_templateName = $name; } } $this->_template = $content; return $this->_template; } protected function set_data($d) { $this->_data = self::megreData($this->_data,$d); return $this->_data; } protected function set_settings($d) { $this->_settings = self::megreData($this->_settings,$d); return $this->_settings; } protected function set_templatePack($v) { if (is_dir(PARSER_TPL_PATH.$v)) $this->_templatePack = $v; return $this->_templatePack; } /* CLASS:METHOD @description : Выполнение сниппет или расширения @param : $name | string | value | | Имя сниппета @param : $A | array | value | @EMPTY | Аргументы @param : $I | string | value | @EMPTY | Входные данные @param : int/string */ public function execute($name,$A=array(),$I='') { $result = ''; /** @noinspection PhpUnusedLocalVariableInspection */ $input = strval($I); /** @noinspection PhpUnusedLocalVariableInspection */ $arguments = $A; if ($fn = $this->search('snippets',$name)) $result = include($fn); return strval($result); } public function parse_chunk($m) { return $this->parse_element($m,'chunk'); } public function parse_constant($m) { return $this->parse_element($m,'constant'); } public function parse_path($m) { return $this->parse_element($m,'path'); } public function parse_deploy($m) { return $this->parse_element($m,'deploy'); } public function parse_setting($m) { return $this->parse_element($m,'setting'); } public function parse_placeholder($m) { return $this->parse_element($m,'placeholder'); } public function parse_debug($m) { return $this->parse_element($m,'debug'); } public function parse_snippet($m) { return $this->parse_element($m,'snippet'); } public function parse_local($m) { return $this->parse_element($m,'local'); } /* CLASS:METHOD @description : Парсинг элемента @param : $m | array | value | | Входные данные @param : $etype | string | value | @EMPTY | Тип элемента @param : string */ private function parse_element($m,$etype='') { $arguments = array(); if (isset($m[8]) && !empty($m[8])) if ($_ = preg_match_all('|\&([\w\-\.]+)\=`([^`]*)`|si',$m[8],$ms,PREG_SET_ORDER)) foreach ($ms as $pr) $arguments[$pr[1]] = $pr[2]; $k = $m[1]; $v = ''; if (empty($etype)) return ''; switch($etype) { case 'chunk': if ($fn = $this->search('chunk',$k)) { $v = @file_get_contents($fn); } else { return "<!-- EMPTY chunk/$k -->"; } break; case 'constant': if (empty($k) || !defined($k)) return "<!-- EMPTY $etype/$k -->"; $v = strval(constant($k)); break; case 'setting': case 'local': case 'placeholder': $PA = array('setting' => '_settings','local' => '_arguments','placeholder' => '_data'); $PN = strval($PA[$etype]); $AR = $this->$PN; // Фикс if (!isset($AR[$k])) return "<!-- EMPTY $etype/$k -->"; $v = strval($AR[$k]); break; case 'debug': $dd = explode('.',$k); $dk = $dd[0]; $asz = array('kb' => 1000,'mb' => 1000000,'gb' => 1000000000); $atm = array('ms' => 1000,'us' => 1000000,'ns' => 1000000000); switch ($dk) { case 'memory': case 'mem': $v = memory_get_usage(); if (count($dd) > 1) if (array_key_exists($dd[1],$asz)) $v /= $asz[$dd[1]]; $v = strval(round($v,2)); break; case 'time': $v = self::microTime(); if (count($dd) > 1) if (array_key_exists($dd[1],$atm)) $v *= $atm[$dd[1]]; $v = strval(round($v,2)); break; case 'totalmem': $v = '<!-- PARSER:TOTALMEM'; if (count($dd) > 1) if (array_key_exists($dd[1],$asz)) $v.= ' '.$dd[1]; $v.= " -->"; break; case 'totaltime': $v = '<!-- PARSER:TOTALTIME'; if (count($dd) > 1) if (array_key_exists($dd[1],$atm)) $v.= ' '.$dd[1]; $v.= " -->"; break; default: $v = ''; } if (empty($v)) { if (!isset($this->_debugTrace[$k])) return "<!-- EMPTY debugtrace/$k -->"; $v = $this->_debugTrace[$k]; } break; case 'snippet': if ($_ = $this->execute($k,$arguments)) { $v = strval($_); } else { if ($_ === false) return "<!-- EMPTY snippet/$k -->"; } break; } if (isset($m[2])) $v = $this->extensions($v,$m[2]); $this->_arguments = $arguments; if (!in_array($etype,array('snippet')) && is_array($arguments)) if (count($arguments) > 0) foreach ($arguments as $phk => $phv) $v = str_replace("[+$phk+]",$phv,$v); return ($v != '') ? $this->parse($v,$etype,$k) : ''; } /* CLASS:METHOD @description : Парсинг @param : $d | string | value | @EMPTY | Входные данные @param : $elt | string | value | @EMPTY | Тип элемента @param : $key | string | value | @EMPTY | Ключ элемента @param : string */ public function parse($d='',$elt='',$key='') { static $_level = -1; static $_levels = null; $P = is_null($_levels) ? 2 : 1; $O = is_null($_levels) ? $this->_template : strval($d); if (is_null($_levels)) $_levels = array(); if (empty($O)) return $O; for ($c = 0; $c < $P; $c++) { $_level++; if ($_level <= self::$_maxLevel) { $_levels[$_level] = array('element' => $elt,'key' => $key); $this->_debugTrace['levels'] = $_levels; foreach (self::$_tags as $k => $t) { if (method_exists($this,"parse_$k")) { if (preg_match($t,$O)) $O = preg_replace_callback($t,array($this,"parse_$k"),$O); } else { throw new Exception("parser not implemented: $k"); } } } else { $O = self::sanitize($O); } $_level--; } if ($_level == -1) { $st = self::microTime(); $tm = self::memoryUsage(); $ph = array( 'TOTALTIME' => round($st,2), 'TOTALTIME ms' => round($st*1000,2), 'TOTALTIME us' => round($st*1000000,2), 'TOTALTIME ns' => round($st*1000000000,2), 'TOTALMEM' => round($tm,2), 'TOTALMEM kb' => round($tm/1000,2), 'TOTALMEM mb' => round($tm/1000000,2), 'TOTALMEM gb' => round($tm/1000000000,2) ); foreach ($ph as $phk => $phv) $O = str_replace("<!-- DEBUG:$phk -->",$phv,$O); $O = self::sanitize($O); $_levels = null; } return $O; } /* CLASS:METHOD @description : Обработка расширений @param : $value | string | value | | Входные данные @param : $ext | string | value | @EMPTY | Тип элемента @param : string */ public function extensions($value,$ext='') { $RET = $value; $TRET = trim($RET); if (empty($ext)) return $value; if ($_ = preg_match_all('|\:([\w\-\.]+)((\=`([^`]*)`)?)|si',$ext,$ms,PREG_SET_ORDER)) { for($c = 0; $c < count($ms); $c++) { $a = $ms[$c][1]; $v = isset($ms[$c][4]) ? $ms[$c][4] : ''; if (in_array($a,array('is','eq','isnot','neq','lt','lte','gt','gte'))) { $cond = false; switch ($a) { case 'is': case 'eq': $cond = ($value == $v); break; case 'isnot': case 'neq': $cond = ($value != $v); break; case 'lt': $cond = ($value < $v); break; case 'lte': $cond = ($value <= $v); break; case 'gt': $cond = ($value > $v); break; case 'gte': $cond = ($value >= $v); break; } $cthen = $RET; if ($ms[$c+1][1] == 'then') { $c++; $cthen = $ms[$c][2]; } $celse = $RET; if ($ms[$c+1][1] == 'else') { $c++; $celse = $ms[$c][2]; } $RET = $cond ? $cthen : $celse; } else { $EMP = (empty($TRET) && ($TRET !== '0')); switch ($a) { case 'empty' : $RET = $EMP ? $v : $TRET; break; case 'notempty': $RET = $EMP ? '' : str_replace('[+value+]',"$TRET",$v); break; default: if ($_ = $this->execute($a,$v,$RET)) $RET = $_; } } } } return $RET; } /* CLASS:METHOD @description : Поиск элемента @param : $type | string | value | | Тип элемента @param : $name | string | value | | Имя элемента @param : string */ public function search($type,$name) { $sdir = 'chunks'; $ext = 'html'; $DS = DIRECTORY_SEPARATOR; switch ($type) { case 'template': $sdir = 'pages'; break; case 'chunk' : break; case 'snippet' : $sdir = 'snippets'; $ext = 'php'; break; default: throw new Exception('No parser type'); break; } $dname = explode('.',$name); $ename = $dname[count($dname)-1]; unset($dname[count($dname)-1]); $dname = count($dname) > 0 ? implode($DS,$dname).$DS : ''; $found = ''; $_ = array(); if (!empty($this->_templatePack)) $_[] = $this->_templatePack; $_[] = $sdir; $path = PARSER_TPL_PATH.implode($DS,$_).$DS.$dname; $fname = $path."$ename.$ext"; if (is_file($fname)) $found = $fname; return $found; } /* Инициализация тегов */ public static function initTags() { if (is_null(self::$_tags)) { $map = array( 'chunk' => array('\{\{','\}\}'), 'constant' => array('\{\*','\*\}'), 'setting' => array('\[\(','\)\]'), 'placeholder' => array('\[\*','\*\]'), 'debug' => array('\[\^','\^\]'), 'snippet' => array('\[\!','\!\]'), 'local' => array('\[\+','\+\]'), ); self::$_tags = array(); foreach ($map as $k => $d) { self::$_tags[$k] = "#".$d[0] . '([\w\.\-]+)' // Alias . '((:?\:([\w\-\.]+)((=`([^`]*)`))?)*)' // Extensions . '((:?\s*\&([\w\-\.]+)=`([^`]*)`)*)' // Parameters . $d[1].'#si'; } } } /* Очистка от тегов */ public static function sanitize($data='') { self::initTags(); if (empty($data)) return ''; $O = $data; foreach (self::$_tags as $t) if (preg_match($t,$O)) $O = preg_replace($t,'',$O); return $O; } public static function microTime() { return microtime(true) - PARSER_STARTTIME; } public static function memoryUsage() { return memory_get_usage() - PARSER_STARTMEM; } public static function megreData($input,$value) { if (!is_array($value) || !is_array($input)) return $input; if (empty($input)) return $value; $ret = $input; foreach ($value as $k => $v) $ret[$k] = $v; return $ret; } } ?>
Пример использования парсера:
<?php require 'parser.class.php'; $parser = new quadBracesParser(); $parser->templatePack = 'default'; // Устанавливаем шаблон-пак $parser->template = 'index'; // Устанавливаем сам шаблон $parser->data = array( 'foo' => 'bar', 'pagetitle' => 'Тестовая страница', 'content' => 'Hello world!' ); $parser->settings = array( 'my_setting' => 'Моя настройка аднака' ); echo $parser; ?>
Поддерживаемые раширения
is, eq, isnot, neq, lt, lte, gt, gte, then, else — базовая логика сравнения
empty — если пусто
notempty — если не пусто
Примеры возможностей
{{path.to.my-chunk}} — выведет обработанное содержимое чанка, находящегося в файле /path/to/my-chunk.html в папке парсера. Как я уже говорил, возможность позволяет структурировать данные шаблонов, не сваливая всё в одну кучу.
{{my-chunk &foo=`bar`}} — выведет обработанное содержимое чанка my-chunk, заменив внутри него плейсхолдер [+foo+] на строку «bar». Эта фича позволяет обходиться без громоздких сниппетов-итераторов. Надо вывести в лендинге десяток проектов? Нет ничего проще! Создаём чанк вывода проекта, а на странице тупо несколько раз его вызываем, подставляя нужные локальные параметры — PROFIT!
[*cool-data &foo=`bar`*] — то же, что и выше, только с плейсхолдером cool-data.
[*cool-data:empty=`bar`*] — если плейсхолдер cool-data будет пустым, выведется строка «bar»
{*MY_CONSTANT*} — выведет содержимое константы MY_CONSTANT, если таковая определена. Работает с любыми константами. Аккуратнее с «волшебными» константами. Иногда случаются любопытные вещи.
[!my.cool.snippet:empty=`пустонафик` &argument=`foobar`!] — Выполнит сниппет my/cool/snippet.php из папки шаблона, передав в качестве аргументов массив, содержащий элемент с индексом argument и значением «foobar». Если результат работы сниппет будет пустым, выведет строку «пустонафик».
В общем бесконечно можно фантазировать о применении этого парсера. По сему экспериментируйте. В комментариях задавайте вопросы, а также пишите об ошибках, если таковые будут. Я проверял — всё работает.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
ссылка на оригинал статьи http://habrahabr.ru/post/266865/
Добавить комментарий