Dirt-простые шаблоны PHP, продолжение

Задний план


Итак, в прошлый раз , когда я спросил о шаблонах PHP, у меня появилось много ответов вроде:

  • он не нужен; PHP – достаточно хороший шаблонный язык сам по себе.
  • сложно разработать язык шаблонов, который одновременно и мощный, и простой для дизайнеров для работы (или вокруг).
  • это уже сделано, используйте шаблонную схему X.
  • ты глупый.

Все эти моменты имеют для них некоторую ценность. Помня их, я продолжил работу с шаблонами, и теперь у меня появилось больше вопросов. 🙂

обзор


цели

Вот цели для этого шаблонизатора:

  • минимальный синтаксис.
  • производить чистый php-код.
  • не нарушайте подсветку синтаксиса html.
  • не нужно, чтобы разработчики PHP узнали что-нибудь новое (ну, не так много).
  • поддерживают большинство управления потоком php (все, кроме do..while).
  • поддержка встроенных php.

Надеюсь, это звучит неплохо. Обратите внимание, что среди целей нет таких вещей, как «предотвращать создание авторов шаблонов» или «шаблоны будут предоставляться анонимными пользователями». Безопасность здесь не является серьезной проблемой, не более, чем в обычном не templated php-файле.

правила

  • escape-последовательность по умолчанию – {{...}} . *
    • если никакие другие правила не совпадают, эхо или оценить последовательность
      • если последовательность заканчивается точкой с запятой, оцените всю последовательность
      • в противном случае, повторите первое выражение и оцените остальные
  • {{for|foreach|if|switch|while (...):}} начинает блок.
    • скобки в состоянии могут быть опущены
    • двоеточие может быть опущено
    • внешняя правая скобка может быть опущена для сопоставления кронштейнов **.
  • {{else|elseif|break|continue|case|default}} делать то, что вы ожидаете.
    • скобки в состоянии могут быть опущены
    • внешняя правая скобка может быть опущена на {{case}} для сопоставления скобок.
    • внешняя левая скобка может быть опущена на {{break|continue}} для сопоставления скобок.
  • {{end}} завершает блок.
    • могут быть добавлены слова «конец», например «end_if»,
    • внешняя левая скобка может быть опущена для сопоставления скобок.

* Пользовательские скобки могут использоваться.
** Синтаксис сопоставления кронштейнов может быть отключен.

шаблонирование

До сих пор мы действительно придумали синтаксис замены для <?php...?> И <?=...?> . Чтобы это было действительно полезно, нам нужны некоторые операции, связанные с шаблонами.

В другой структуре шаблонов, в которой я работал, используется простая парадигма контейнера / контента, которая должна хорошо работать здесь. Эта система шаблонов была основана на xml, поэтому код выглядел бы примерно так …

 <!-- in a template --> <html> <head> <tt:Container name="script" /> </head> <body> <tt:Container name="main" /> </body> </html> <!-- in a page --> <tt:Content name="script"> <script src="foo.js"></script> </tt:Content> <tt:Content name="main"> <div>...</div> </tt:Content> 

Несколько объявлений области содержимого с тем же именем заменят предыдущий контент, но предыдущий контент будет доступен в теге Content через Container, поэтому:

 <tt:Content name="script"> <script src="foo.js"></script> </tt:Content> ... <tt:Content name="script"> <script src="bar.js"></script> <tt:Container name="script" /> </tt:Content> ... <tt:Container name="script" /> 

Должен выводиться:

  <script src="bar.js"></script> <script src="foo.js"></script> 

Я попытался воссоздать Content и Container через set и get теги в этой новой системе шаблонов. Они предназначены для работы точно так же, за исключением, конечно, не тегов xml.

Код


Без дальнейших церемоний:

 <?php class Detemplate { public $container_prefix='_tpl_'; public $brackets='{}'; public $bracket_matching=true; public $use_cache=false; private $block_keywords=array('for','foreach','if','switch','while'); private $set_count; private $get_count; public function parse_file ($file, $vars=array()) { $sha1=sha1($file); $cache = dirname(__FILE__)."/cache/".basename($file).".$sha1.php"; $f = "{$this->container_prefix}page_{$sha1}_"; if (!$this->use_cache || !file_exists($cache) || filemtime($cache)<filemtime($file)) { $php = "<?php function $f {$this->t_vars()} ?>". $this->parse_markup(file_get_contents($file)). "<?php } ?>"; file_put_contents($cache, $php); } include $cache; $f($vars); } public function parse_markup ($markup) { $blocks=implode('|', $this->block_keywords); $arglist= '\s*[\s(](.*?)\)?\s*'; // capture an argument list $word= '\s*(\w+)\s*'; // capture a single word $l='\\'.$this->brackets{0}; // left bracket $r='\\'.$this->brackets{1}; // right bracket $dl="#$l$l"; $sl=$this->bracket_matching ? "#$l?$l" : $dl; $dr="$r$r(?!:$r)#"; $sr=$this->bracket_matching ? "$r$r?(?!:$r)#" : $dr; $markup=preg_replace_callback( array ( $sl.'(end)[_\w]*\s*;?\s*'.$dr, $dl.'(el)se\s*if'.$arglist.':?\s*'.$dr, $dl.'(else)\s*:?\s*'.$dr, $dl.'(case)'.$word.':?\s*'.$sr, $dl.'(default)()\s*:?\s*'.$sr, $sl.'(break|continue)\s*;?\s*'.$dr, $dl.'(set)'.$word.':?\s*'.$sr, $dl.'(get)'.$word.':?\s*'.$dr, $dl.'(parse)'.$word.':?\s*'.$dr, $dl.'(function|fn)'.$word.$arglist.':?\s*'.$sr, $dl.'('.$blocks.')'.$arglist.':?\s*'.$sr, '#('.$l.$l.')(.+?)(;?)\s*'.$dr, '#\s*(\?)>[\s\n]*<\?php\s*#', ), array($this, 'preg_callback'), $markup); return $markup; } private function preg_callback ($m) { switch ($m[1]) { // end of block case "end": return "<?php } } ?>"; // keywords with special handling case "el": // elseif return "<?php } elseif ({$m[2]}) { ?>"; case "else": return "<?php } else { ?>"; case "case": case "default": return "<?php {$m[1]} {$m[2]}: ?>"; case "break": case "continue": return "<?php {$m[1]}; ?>"; // parse an external template document case "parse": return $this->parse_markup(file_get_contents($m[2])); // save / load content sections case "set": $i=++$this->set_count[$m[2]]; $f=$this->t_fn($m[2], $i); $p=$this->t_fn($m[2], $i-1); $v=$this->t_fn_alias($m[2]); return "<?php if (!function_exists('$f')) { $v='$f'; ". "function $f {$this->t_vars()} unset ($v); $v='$p'; ?>"; case "get": $i=++$this->get_count[$m[2]]; $c=$this->t_fn_ctx($m[2], $i); $v=$this->t_tmp(); $a=$this->t_fn_alias($m[2]); return "<?php if (!$c) { ". "foreach (array_keys(get_defined_vars()) as $v) $c". "[$v]=&\$$v; unset($v); } $a(&$c); ?>"; case "function": case "fn": return "<?php if (!function_exists('{$m[2]}')) { ". "function {$m[2]} ({$m[3]}) { ?>"; // echo / interpret case "{{": return "<?php".($m[3]?"":" echo")." {$m[2]}; ?>"; // merge adjacent php tags case "?": return " "; } // block keywords if (in_array($m[1], $this->block_keywords)) { return "<?php { {$m[1]} ({$m[2]}) { ?>"; } } private function t_fn ($name, $index) { if ($index<1) return "is_null"; return "{$this->container_prefix}{$name}_$index"; } private function t_fn_alias ($name) { return "\${$this->container_prefix}['fn_$name']"; } private function t_fn_ctx ($name, $index) { return "\${$this->container_prefix}['ctx_{$name}_$index']"; } private function t_vars () { $v=$this->t_tmp(); return "($v) { extract($v); unset($v);"; } private function t_tmp () { return '$'.$this->container_prefix.'v'; } } ?> , <?php class Detemplate { public $container_prefix='_tpl_'; public $brackets='{}'; public $bracket_matching=true; public $use_cache=false; private $block_keywords=array('for','foreach','if','switch','while'); private $set_count; private $get_count; public function parse_file ($file, $vars=array()) { $sha1=sha1($file); $cache = dirname(__FILE__)."/cache/".basename($file).".$sha1.php"; $f = "{$this->container_prefix}page_{$sha1}_"; if (!$this->use_cache || !file_exists($cache) || filemtime($cache)<filemtime($file)) { $php = "<?php function $f {$this->t_vars()} ?>". $this->parse_markup(file_get_contents($file)). "<?php } ?>"; file_put_contents($cache, $php); } include $cache; $f($vars); } public function parse_markup ($markup) { $blocks=implode('|', $this->block_keywords); $arglist= '\s*[\s(](.*?)\)?\s*'; // capture an argument list $word= '\s*(\w+)\s*'; // capture a single word $l='\\'.$this->brackets{0}; // left bracket $r='\\'.$this->brackets{1}; // right bracket $dl="#$l$l"; $sl=$this->bracket_matching ? "#$l?$l" : $dl; $dr="$r$r(?!:$r)#"; $sr=$this->bracket_matching ? "$r$r?(?!:$r)#" : $dr; $markup=preg_replace_callback( array ( $sl.'(end)[_\w]*\s*;?\s*'.$dr, $dl.'(el)se\s*if'.$arglist.':?\s*'.$dr, $dl.'(else)\s*:?\s*'.$dr, $dl.'(case)'.$word.':?\s*'.$sr, $dl.'(default)()\s*:?\s*'.$sr, $sl.'(break|continue)\s*;?\s*'.$dr, $dl.'(set)'.$word.':?\s*'.$sr, $dl.'(get)'.$word.':?\s*'.$dr, $dl.'(parse)'.$word.':?\s*'.$dr, $dl.'(function|fn)'.$word.$arglist.':?\s*'.$sr, $dl.'('.$blocks.')'.$arglist.':?\s*'.$sr, '#('.$l.$l.')(.+?)(;?)\s*'.$dr, '#\s*(\?)>[\s\n]*<\?php\s*#', ), array($this, 'preg_callback'), $markup); return $markup; } private function preg_callback ($m) { switch ($m[1]) { // end of block case "end": return "<?php } } ?>"; // keywords with special handling case "el": // elseif return "<?php } elseif ({$m[2]}) { ?>"; case "else": return "<?php } else { ?>"; case "case": case "default": return "<?php {$m[1]} {$m[2]}: ?>"; case "break": case "continue": return "<?php {$m[1]}; ?>"; // parse an external template document case "parse": return $this->parse_markup(file_get_contents($m[2])); // save / load content sections case "set": $i=++$this->set_count[$m[2]]; $f=$this->t_fn($m[2], $i); $p=$this->t_fn($m[2], $i-1); $v=$this->t_fn_alias($m[2]); return "<?php if (!function_exists('$f')) { $v='$f'; ". "function $f {$this->t_vars()} unset ($v); $v='$p'; ?>"; case "get": $i=++$this->get_count[$m[2]]; $c=$this->t_fn_ctx($m[2], $i); $v=$this->t_tmp(); $a=$this->t_fn_alias($m[2]); return "<?php if (!$c) { ". "foreach (array_keys(get_defined_vars()) as $v) $c". "[$v]=&\$$v; unset($v); } $a(&$c); ?>"; case "function": case "fn": return "<?php if (!function_exists('{$m[2]}')) { ". "function {$m[2]} ({$m[3]}) { ?>"; // echo / interpret case "{{": return "<?php".($m[3]?"":" echo")." {$m[2]}; ?>"; // merge adjacent php tags case "?": return " "; } // block keywords if (in_array($m[1], $this->block_keywords)) { return "<?php { {$m[1]} ({$m[2]}) { ?>"; } } private function t_fn ($name, $index) { if ($index<1) return "is_null"; return "{$this->container_prefix}{$name}_$index"; } private function t_fn_alias ($name) { return "\${$this->container_prefix}['fn_$name']"; } private function t_fn_ctx ($name, $index) { return "\${$this->container_prefix}['ctx_{$name}_$index']"; } private function t_vars () { $v=$this->t_tmp(); return "($v) { extract($v); unset($v);"; } private function t_tmp () { return '$'.$this->container_prefix.'v'; } } ?> по <?php class Detemplate { public $container_prefix='_tpl_'; public $brackets='{}'; public $bracket_matching=true; public $use_cache=false; private $block_keywords=array('for','foreach','if','switch','while'); private $set_count; private $get_count; public function parse_file ($file, $vars=array()) { $sha1=sha1($file); $cache = dirname(__FILE__)."/cache/".basename($file).".$sha1.php"; $f = "{$this->container_prefix}page_{$sha1}_"; if (!$this->use_cache || !file_exists($cache) || filemtime($cache)<filemtime($file)) { $php = "<?php function $f {$this->t_vars()} ?>". $this->parse_markup(file_get_contents($file)). "<?php } ?>"; file_put_contents($cache, $php); } include $cache; $f($vars); } public function parse_markup ($markup) { $blocks=implode('|', $this->block_keywords); $arglist= '\s*[\s(](.*?)\)?\s*'; // capture an argument list $word= '\s*(\w+)\s*'; // capture a single word $l='\\'.$this->brackets{0}; // left bracket $r='\\'.$this->brackets{1}; // right bracket $dl="#$l$l"; $sl=$this->bracket_matching ? "#$l?$l" : $dl; $dr="$r$r(?!:$r)#"; $sr=$this->bracket_matching ? "$r$r?(?!:$r)#" : $dr; $markup=preg_replace_callback( array ( $sl.'(end)[_\w]*\s*;?\s*'.$dr, $dl.'(el)se\s*if'.$arglist.':?\s*'.$dr, $dl.'(else)\s*:?\s*'.$dr, $dl.'(case)'.$word.':?\s*'.$sr, $dl.'(default)()\s*:?\s*'.$sr, $sl.'(break|continue)\s*;?\s*'.$dr, $dl.'(set)'.$word.':?\s*'.$sr, $dl.'(get)'.$word.':?\s*'.$dr, $dl.'(parse)'.$word.':?\s*'.$dr, $dl.'(function|fn)'.$word.$arglist.':?\s*'.$sr, $dl.'('.$blocks.')'.$arglist.':?\s*'.$sr, '#('.$l.$l.')(.+?)(;?)\s*'.$dr, '#\s*(\?)>[\s\n]*<\?php\s*#', ), array($this, 'preg_callback'), $markup); return $markup; } private function preg_callback ($m) { switch ($m[1]) { // end of block case "end": return "<?php } } ?>"; // keywords with special handling case "el": // elseif return "<?php } elseif ({$m[2]}) { ?>"; case "else": return "<?php } else { ?>"; case "case": case "default": return "<?php {$m[1]} {$m[2]}: ?>"; case "break": case "continue": return "<?php {$m[1]}; ?>"; // parse an external template document case "parse": return $this->parse_markup(file_get_contents($m[2])); // save / load content sections case "set": $i=++$this->set_count[$m[2]]; $f=$this->t_fn($m[2], $i); $p=$this->t_fn($m[2], $i-1); $v=$this->t_fn_alias($m[2]); return "<?php if (!function_exists('$f')) { $v='$f'; ". "function $f {$this->t_vars()} unset ($v); $v='$p'; ?>"; case "get": $i=++$this->get_count[$m[2]]; $c=$this->t_fn_ctx($m[2], $i); $v=$this->t_tmp(); $a=$this->t_fn_alias($m[2]); return "<?php if (!$c) { ". "foreach (array_keys(get_defined_vars()) as $v) $c". "[$v]=&\$$v; unset($v); } $a(&$c); ?>"; case "function": case "fn": return "<?php if (!function_exists('{$m[2]}')) { ". "function {$m[2]} ({$m[3]}) { ?>"; // echo / interpret case "{{": return "<?php".($m[3]?"":" echo")." {$m[2]}; ?>"; // merge adjacent php tags case "?": return " "; } // block keywords if (in_array($m[1], $this->block_keywords)) { return "<?php { {$m[1]} ({$m[2]}) { ?>"; } } private function t_fn ($name, $index) { if ($index<1) return "is_null"; return "{$this->container_prefix}{$name}_$index"; } private function t_fn_alias ($name) { return "\${$this->container_prefix}['fn_$name']"; } private function t_fn_ctx ($name, $index) { return "\${$this->container_prefix}['ctx_{$name}_$index']"; } private function t_vars () { $v=$this->t_tmp(); return "($v) { extract($v); unset($v);"; } private function t_tmp () { return '$'.$this->container_prefix.'v'; } } ?> 

Пример шаблона html:

 <script>var _lang = {{json_encode($lang)}};</script> <script src='/cartel/static/inventory.js'></script> <link href='/cartel/static/inventory.css' type='text/css' rel='stylesheet' /> <form class="inquiry" method="post" action="process.php" onsubmit="return validate(this)"> <div class="filter"> <h2>{{$lang['T_FILTER_TITLE']}}</h2> <a href='#{{urlencode($lang['T_FILTER_ALL'])}}' onclick='applyFilter();'>{{$lang['T_FILTER_ALL']}}</a> {{foreach ($filters as $f)}} <a href='#{{urlencode($f)}}' onclick='applyFilter("c_{{urlencode($f)}}");'>{{$f}}</a> {{end}} </div> <table class="inventory" id="inventory_table"> {{foreach $row_array as $row_num=>$r} {{if $row_num==0} <tr class='static'> {{foreach $r as $col} <th>{{$col}}</th> {end}} <th class='ordercol'>{{$lang['T_ORDER']}}</th> </tr> {{else}} {{function spin_button $id, $dir, $max} <a href='#' class='spinbutton' onclick="return spin('{{$id}}', {{$dir}}, {{$max}})"> {{$dir==-1 ? '◀' : '▶'}} </a> {end}} <tr class="{{'c_'.urlencode($r[$man_col])}}"> {{foreach $r as $i=>$col} <td class='{{$i?"col":"firstcol"}}'>{{$col}}</td> {end}} <td class='ordercol'> {{$id="part_{$r[$part_col]}"; $max=$r[$qty_col];}} {{spin_button($id, -1, $max)}} <input onchange="spin(this.id, 0, '{{$max}}')" id='{{$id}}' name='{{$id}}'type='text' value='0' /> {{spin_button($id, +1, $max)}} </td> </tr> {end}} {end}} <tr class="static"><th colspan="{{$cols+1}}">{{$lang['T_FORM_HELP']}}</th></tr> {{foreach $fields as $f} <tr class="static"> <td class="fields" colspan="2"> <label for="{{$f[0]}}">{{$f[1]}}</label> </td> <td class="fields" colspan="{{$cols-1}}"> <input name="{{$f[0]}}" id="{{$f[0]}}" type="text" /> </td> </tr> {end}} <tr class="static"> <td id="validation" class="send" colspan="{{$cols}}">&nbsp;</td> <td colspan="1" class="send"><input type="submit" value="{{$lang['T_SEND']}}" /></td> </tr> </table> </form> 

Вопросов


У меня есть несколько вопросов о том, как это сделать. У некоторых есть определенные ответы, некоторые могут быть больше материала CW …

  • set / get создает беспорядочный код. Можно ли улучшить? Я ищу какую-то разумную середину между set / get и {{function}} (см. Код и пример).

  • Чего не хватает на популярных языках шаблонов?

  • Синтаксис ок? Должны ли строки, которые эхо-вещи, линии, которые делают вещи, и линии управления потоком, более синтаксически различны? Как насчет дополнительных внешних кронштейнов для сопоставления … глупо?

Мы с нетерпением ждем, когда все будут проинформированы об этом.

минимальный синтаксис.

<?=$variable?> и http://phptemplatinglanguage.com/

производить чистый php-код.

Это не выглядит очень чистым для меня.

не нарушайте подсветку синтаксиса html.

Вы нарушаете подсветку синтаксиса PHP, который я нахожу более проблематичным, чем разбиение подсветки синтаксиса HTML. Если вы получите лучший редактор, который понимает, как взаимодействуют PHP и HTML (я использую Textmate), это даже не проблема.

не нужно, чтобы разработчики PHP узнали что-нибудь новое (ну, не так много).

Обычный PHP уже квалифицируется.

поддерживают большинство управления потоком php (все, кроме do..while).

Обычный PHP поддерживает весь контроль потока PHP.

поддержка встроенных php.

Обычный PHP поддерживает встроенный PHP.

Таким образом, я не вижу преимуществ для этого подхода, конечно, не для зрелых существующих фреймворков PHP и шаблонов.