QuadBraces — по мотивам парсера MODx

от автора

Доброго здравия хабражителям!

Когда я заглянул в исходники 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». Если результат работы сниппет будет пустым, выведет строку «пустонафик».

В общем бесконечно можно фантазировать о применении этого парсера. По сему экспериментируйте. В комментариях задавайте вопросы, а также пишите об ошибках, если таковые будут. Я проверял — всё работает.

А Вы делаете лендинги на MODx

Никто ещё не голосовал. Воздержавшихся нет.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

ссылка на оригинал статьи http://habrahabr.ru/post/266865/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *