Шаблонизатор VMX::Template
Данный модуль представляет собой новую версию VMX::Template, построенную на некоторых новых идеях, ликвидировавшую безобразие и legacy-код, накопленный в старой версии, однако сохранившую высокую производительность и простоту.
Есть PHP-версия и Perl-версия шаблонизатора. Реализация, естественно, несколько отличается по причине различий языков — например, в Perl’е для кэширования кода используются coderef’ы, а в PHP предполагается, что кэшированием занимается какой-нибудь XCache или eAccelerator, ибо там сохранить coderef между запросами, по-видимому, невозможно.
Развивается то одна, то другая, в зависимости от проекта, над которым я работаю в моменте.
Также есть простенький (и кривоватенький) файл настроек синтаксиса шаблонов для Midnight Commander'а: tpl.syntax.
Работаю над переводчиком с Template::Toolkit на VMX::Template. (ибо TT — задрал, скотина!)
Про VMX::Template можно сказать «ох уж эти перлисты — что ни пишут, всё Template::Toolkit получается». Это к тому, что идея вообще-то схожая, но реализация гораздо проще и быстрее.
Содержание
- 1 Идеи
- 2 Использование
- 3 Реализация
- 4 Функции
- 4.1 Числа, логические операции
- 4.2 Строки
- 4.2.1 LC=LOWER=LOWERCASE, UC=UPPER=UPPERCASE
- 4.2.2 Q=QUOTE=ADDSLASHES, SQ=SQL_QUOTE, REQUOTE=RE_QUOTE=PREG_QUOTE
- 4.2.3 URI_QUOTE=URIQUOTE=URLENCODE
- 4.2.4 REPLACE, STR_REPLACE
- 4.2.5 STRLEN
- 4.2.6 SUBSTR=SUBSTRING
- 4.2.7 TRIM
- 4.2.8 SPLIT
- 4.2.9 S=HTML=HTMLSPECIALCHARS, T=STRIP, H=STRIP_UNSAFE, NL2BR
- 4.2.10 CONCAT, JOIN=IMPLODE
- 4.2.11 SUBST, SPRINTF, STRFTIME
- 4.2.12 STRLIMIT
- 4.3 Массивы и хеши
- 4.4 Прочее
Идеи
Уйти от assign_vars(), assign_block_vars(). Передавать, как и в обычных движках, просто хеш с данными $vars. Как, например, в Template::Toolkit. При этом сохранить данные методы для совместимости.
Почистить синтаксис: ликвидировать «преобразования», «вложенный путь по переменной» (->key->index->key->и т. п.), специальный синтаксис для окончания SET, неочевидное обращение к счётчику block.#, tr_assign_* и т. п.
Переписать с нуля компилятор.
Добавить в употребление функции, но только самые необходимые.
Добавить обработку ошибок и диагностические сообщения.
Использование
Здесь можно прочитать об: использовании в PHP, использовании в Perl, различиях реализаций.
PHP
require_once 'template.php'; # Конструктор $template = new Template(array( 'root' => '.', # директория с шаблонами 'cache_dir' => './cache', # директория для кэширования компилированного кода шаблонов 'print_error' => true, # если true, ошибки компиляции выводятся на STDOUT 'raise_error' => false, # если true, при ошибке компиляции вызывается die() 'reload' => true, # если false, шаблоны будут считываться с диска только 1 раз, и вызовов stat() происходить не будет 'use_utf8' => true, # если true, использовать кодировку UTF-8 для строковых операций 'begin_code' => '<!--', # маркер начала директивы кода 'end_code' => '-->', # маркер конца директивы кода 'eat_code_line' => true, # (похоже на TT CHOMP) съедать "лишний" перевод строки, если в строке только директива? 'begin_subst' => '{', # маркер начала подстановки выражения 'end_subst' => '}', # маркер конца подстановки выражения 'compiletime_functions' => # дополнительные компилируемые функции array('func' => callback), # хеш вида имя функции (в шаблонах) => callback, # которому передаются скомпилированные выражения всех аргументов # немного legacy, устаревшее: 'wrapper' => NULL, # если равно чему-то, что можно вызвать, через это будет # пропущен вывод всех шаблонов ("глобальный фильтр") 'strict_end' => false, # требовать <!-- END имя_блока --> после <!-- BEGIN имя_блока --> )); # Присвоение одной переменной: $template->vars("ключ", "значение"); # Присвоение кучи переменных: $template->vars(array("ключ" => "значение", ...)); # Выполнение шаблона и получение результата: # (возможно с передачей целого хеша данных) $page = $template->parse('имя_файла' [, array("ключ" => "значение", ...)]); # Аналогично выполнение именованного блока из файла: $page = $template->parse('имя_файла', 'имя_блока' [, array("ключ" => "значение", ...)]); # Аналогично выполнение кода из строки: $page = $template->parse(NULL, 'код' [, 'имя_блока'] [, array("ключ" => "значение", ...)]); # Очистка сохранённых данных для генерации ещё одной страницы: $template->clear;
Perl
use VMX::Template; # Конструктор $template = new VMX::Template( 'root' => '.', # директория с шаблонами 'cache_dir' => undef, # директория для кэширования компилированного кода шаблонов 'reload' => 2, # если 0, то шаблоны не будут перечитываться с диска, и вызовов stat() происходить не будет # если >0, то шаблоны будут перечитываться с диска не чаще чем раз в reload секунд 'print_error' => 1, # если TRUE, ошибки компиляции попадают в вывод шаблона 'raise_error' => 0, # если TRUE, при ошибке компиляции вызывается die() 'use_utf8' => undef, # если TRUE, использовать "use utf8" на шаблонах 'begin_code' => '<!--', # маркер начала директивы кода 'end_code' => '-->', # маркер конца директивы кода 'eat_code_line' => 1, # (похоже на TT CHOMP) съедать "лишний" перевод строки, если в строке только директива? 'begin_subst' => '{', # маркер начала подстановки выражения 'end_subst' => '}', # маркер конца подстановки выражения 'compiletime_functions' => # дополнительные компилируемые функции { 'func' => sub {} }, # хеш вида имя функции (в шаблонах) => coderef, # которому передаются скомпилированные выражения всех аргументов и первым - сам $template # немного legacy, устаревшее: 'wrapper' => undef, # если coderef, через это будет пропущен вывод всех шаблонов ("глобальный фильтр") 'strict_end' => 0, # требовать <!-- END имя_блока --> после <!-- BEGIN имя_блока --> ); # Присвоение переменных: $template->vars("ключ" => "значение", "ключ" => "значение", ...); # Выполнения полностью аналогичны PHP: $page = $template->parse('имя_файла' [, 'имя_блока'] [, { "ключ" => "значение", ... } ]); $page = $template->parse('', 'код' [, 'имя_блока'] [, { "ключ" => "значение", ... }]); # Очистка сохранённых данных для генерации ещё одной страницы: $template->clear;
Различия
Кэширование работает по-разному.
В целом, общий смысл — сделать так, чтобы шаблоны было не стыдно вызывать много раз, как много раз за один запрос, так и в целом, при этом максимально использовать механизмы интерпретатора самого языка. Но механизмы для этого применяются разные. Основная причина следующая:
- Perl: считается, что всё прогрессивное человечество уже давно использует mod_perl или другие способы запуска веб-приложений, при которых частых переинициализаций интерпретатора не происходит. Иными словами, никто больше не использует CGI. Таким образом, мы смело можем сохранить живой coderef (ссылку на функцию, или кому как больше нравится — анонимную функцию, замыкание, делегат) в промежутке между двумя запросами.
- PHP: интерпретатор PHP инициализируется заново при обработке каждого HTTP-запроса. А coderef в промежутке между двумя инициализациями интерпретатора сохранить, видимо, невозможно.
В PHP также есть ещё одна проблема — в процессе выполнения невозможно добавить метод в класс без использования извращений типа classkit, а хочется, потому что сгенерированные из кода шаблона функции должны быть методами — они используют контекст класса Template.
Поэтому компилированный шаблон PHP-версии — это класс, производный от класса Template. Единожды за один HTTP-запрос он загружается в память, а при каждом вызове шаблона создаётся пустой объект этого класса, в него записывается ссылка на tpldata и поле parent, ссылающееся на родительский объект Template, и вызывается метод класса, соответствующий блоку шаблона (см. #Блоки).
Кроме кэширования классов в рамках запроса в PHP существует ещё две ступени:
- Текст шаблонов кэшируется в XCache или eAccelerator, если таковые присутствуют, и не перезагружается с диска лишний раз. Если reload = false, лишними считаются все разы, кроме первого, даже если файл шаблона менялся.
- Компилированный код шаблонов кэшируется в файлах на диске, и не компилируется лишний раз.
В Perl действие reload немного отличается — reload = 0 работает так же, как reload = false в PHP, но если reload > 0, то тексты шаблонов перезагружаются с диска при изменении, но не чаще, чем раз в reload секунд. В остальном всё проще — компилированный шаблон представляет собой просто хеш с набором анонимных функций, которые сохраняются в my-переменной пакета VMX::Template и вызываются при обращении к шаблону или его блокам. Также существует и файловый кэш компилированного кода.
Несколько различается действие use_utf8 = true.
- Общий смысл — «мои шаблоны и страницы в кодировке UTF-8».
- PHP: «использовать mb_str* функции для работы со строками в выражениях».
- Perl: «я передаю в шаблон все переменные с флагом UTF-8 = On, их можно смело конкатенировать с UTF-ными частями шаблона». Если кто-то не знает, в Perl строки имеют на себе флаг UTF-8 = да или нет, и при конкатенации строки без флага со строкой с флагом строка без флага будет автоматически переведена в UTF-8 из кодировки, соответствующей текущей локали. Что означает двойное UTF-8-кодирование в случае, если строка на самом деле всё-таки в UTF-8, но просто на ней не установлен флаг.
- Для приведения всех переменных шаблона к UTF-8 можно использовать функцию utf8on() из VMX::Common (рекурсивный Encode::_utf8_on()).
Различается способ вывода ошибок при print_error = true.
- Общий смысл — при print_error = true ошибки и предупреждения должны попасть на экран.
- PHP: они просто выводятся print()'ами.
- Perl: здесь так нельзя, потому что HTTP-заголовки сами могут и не отправиться, поэтому текст ошибок прицепляется к выводу шаблонизатора (возвращается вместе с результатом parse()).
Различаются аргументы, передаваемые в compiletime_functions.
- PHP: просто список кода выражений всех аргументов вызова. Функция-компилятор вызывается вне контекста объекта.
- Perl: тот же список + $self (объект VMX::Template) в качестве первого элемента.
Различается поведение функций сравнения.
- PHP: EQ и т. п. без S/N — типозависимое сравнение.
- Perl: EQ и т. п. без S/N эквивалентно строковому (Sxx).
Различается поведение некоторых функций работы с массивами и хешами.
- KEYS — в PHP порядок ключей массива/хеша сохраняется, а в Perl — нет и принимаются только хеши. Обусловлено реализацией хешей в этих языках.
- EACH — в Perl-версии ключи будут отсортированы по имени.
- RANGE — в Perl-версии принимает буквенные аргументы (A..Z = весь алфавит).
- IS_ARRAY — в PHP-версии не проверяется, а не является ли он при этом хэшем, ибо трудоёмко (надо проверить, численные ли все ключи).
- AGET и HGET в PHP идентичны GET.
- ARRAY_MERGE: под Perl — только массивы (не хеши), под PHP — любые массивы.
- DUMP — это Dumper в Perl’е и var_dump в PHP.
PHP-версия не зависит ни от чего (кроме PHP 5), а Perl-версия зависит от VMX::Common.
В PHP-версии в шаблоны не включаются C-подобные «прагмы» #line, а в текст ошибок не включается имя файла шаблона и строка. Ибо решил — раз уж #line не поддерживается, нечего на строки заморачиваться.
Реализация
Маркеры начала и конца кода <!-- --> и подстановки { } могут быть заменены любыми другими. Если, например, вы привыкли к TT, можно установить [% %]. Маркеры подстановки можно вообще убрать, ибо подстановка тоже является кодом.
Путь к переменной теперь может включать в себя числа. Это будут обращения к элементам массивов, в то время как всё остальное — обращения к элементам хешей.
Итак, <!-- FOR x = y --> — директива кода, {a.b.0.c} — подстановка выражения. Комментарии: <!--# комментарий -->
Циклы
Вне блока {block} будет иметь значение ARRAY(0x…), то есть массив всех итераций блока block, а {block.0} будет иметь значение HASH(0x…), то есть первую итерацию блока block.
<!-- BEGIN block -->
Теперь, внутри блока {block} теперь будет иметь значение HASH(0x…), то есть уже значение текущей итерации блока block, а {block.#} будет иметь значением номер текущей итерации блока, отсчитываемый с 0, а не с 1, как в старой версии.
<!-- END block -->
На <!-- END другоеимя --> после <!-- BEGIN block --> при strict_end = true шаблонизатор выдаст ошибку, «ибо нефиг» (c). Если block в хеше данных — не массив, а хеш — это значит, что итерация у блока только одна, и тогда <!-- BEGIN block --> работает как for($expression) {} в Perl никак.
BEGIN ... END — это циклы в «старом стиле». А можно использовать и TT-подобный, обычно более удобный:
<!-- FOR var = expression --> ... <!-- END -->
Причём, если expression ::= block, то var может быть само block'ом. Это, по сути, и есть то, что делает BEGIN: <!-- BEGIN block --> эквивалентно <!-- FOR block = block -->. Предыдущее значение переменной цикла после выхода из цикла всегда восстанавливается.
К номеру итерации можно обратиться через {var#}.
Функции
Операторов нет, фильтров нет, есть функции. Пример:
<!-- IF OR(function(block.key1),AND(block.key2,block.key3)) -->
Почему? Тут всё просто — основываясь на предположении, что длинные выражения в шаблонах нужны очень редко, было лениво писать нормальную грамматику для разбора обычных выражений. Почему они нужны редко? Да просто минимум логики в шаблонах — признак хороших шаблонов. А функции покрывают сразу и выражения, и «фильтры», и методы объектов.
Синтаксис вызова функции нескольких аргументов:
<!-- function(block.key, 0, "abc") -->
Подстановка:
{function(block.key, 0, "abc")}
Синтаксис вызова функции одного аргумента:
<!-- function(block.key) --> <!-- function block.key --> {block.key/s} {s block.key}
Синтаксис вызова метода объекта:
{object.method()} {object.method(arg1, arg2)} {call(object, "method")} {call(object, "method", array(arg1, arg2))}
Последние два применения — как нетрудно заметить, обращение к функции call() и служат для вызова метода по вычисляемому имени.
Цепочки вызовов методов типа object.method().another_method() не поддерживаются, ибо к ним без сохранения звеньев нервно относится даже сам PHP.
IF
Условный вывод:
<!-- IF function(block.key) --><!-- ELSEIF ... --><!-- END --> <!-- IF NOT block.key -->...<!-- END -->
ELSIF эквивалентно ELSE IF и ELSEIF.
SET
Запись значения переменной:
<!-- SET block.key -->...<!-- END --></nowiki> <!-- SET block.key = выражение -->
Включения
Включение другого шаблона также осталось:
<!-- INCLUDE another-file.tpl --> <!-- INCLUDE "another-file.tpl" -->
По «динамическому» имени шаблона включение производится функцией INCLUDE. Как несложно заметить, вторая строка — как раз вызов функции.
Блоки
Блок — это часть шаблона, выделенная в отдельную «функцию», хорошо кэшируемая и предназначенная для повторного вызова из других мест. Покрывает сразу несколько вещей — «блоки», «макросы» и «обёртки» из TT. Да-да, TT славится бессмысленным дублированием функционала.
Наши блоки имеют несколько преимуществ:
- блоки, определённые в одном шаблоне, можно смело вызывать из других по имени файла + имени блока!
- блок можно определить просто как некоторое выражение.
- блоки хорошо кэшируются — с VMX::Template вы не испытаете разочарования, если вызовете какой-нибудь блок 1000 раз. В отличие от TT.
Блоки в шаблоне не могут быть вложенными, а циклы, SET и прочие вещи, их оборачивающие, не имеют на них никакого влияния. После компиляции блоки просто вырезаются и преобразуются в отдельные функции PHP/Perl’а.
<!-- BLOCK имя_блока --> ...код... <!-- END -->
или
<!-- BLOCK имя_блока = выражение -->
Вместо слова BLOCK можно также использовать слово FUNCTION или MACRO.
Вызывать блок из шаблона следует с помощью функции PROCESS. Вызывать блок из кода следует, передавая после имени файла шаблона имя блока. См. #Использование (PHP).
Функции
Первое, что обычно нужно — это S(), H(), T(), Q(), I(), то есть «фильтры» для различных преобразований строки:
- S() — это htmlspecialchars(), экранирует HTML/XML-спецсимволы в строках.
- H() — удаляет все HTML-теги, кроме «безопасных».
- T() — удаляет все HTML-теги.
- Q() — это addslashes(), экранирует символы для использования, например, в JS.
- I() — преобразует значение к целому числу.
Расширяемость в области функций:
- Run-time функции
- В качестве функции можно использовать метод переданного в хеше данных объекта. В «функцию» можно вынести и блок кода из шаблона — см. #Блоки. Оно хорошо кэшируется.
- Compile-time функции
- При создании объекта шаблона можно передать параметр compiletime_functions, равный хешу, в котором ключи — имена дополнительных функций, а значения — любые coderef’ы (Perl) или callable (PHP). Эти функции вызываются в контексте объекта шаблона с параметрами, равными коду для вычисления соответствующего аргумента, и должны возвращать код для вычисления результата. То есть, они выполняются на этапе компиляции.
Числа, логические операции
OR, AND, NOT
Логические ИЛИ, И, НЕ, действующие аналогично Perl операторам ||, &&, !.
ADD, SUB, MUL, DIV, MOD
Арифметические операции + — * / %.
LOG
Логарифм.
EVEN, ODD
Истина в случае, если аргумент чётный или нечётный соответственно.
INT=I=INTVAL
Преобразование к целому числу.
EQ, NE, GT, LT, GE, LE
Сравнения == != > < >= <= аргументов как строк (Perl) или типо-зависимое сравнение (PHP). В PHP если хотя бы один из аргументов численный, сравниваются они как числа.
SEQ, SNE, SGT, SLT, SGE, SLE
Аргументы сравниваются всегда как строки.
NEQ, NNE, NGT, NLT, NGE, NLE
Аргументы сравниваются всегда как числа.
YESNO
Тернарный оператор $1 ? $2 : $3.
Строки
LC=LOWER=LOWERCASE, UC=UPPER=UPPERCASE
Нижний и верхний регистр.
Q=QUOTE=ADDSLASHES, SQ=SQL_QUOTE, REQUOTE=RE_QUOTE=PREG_QUOTE
Экранирование символов " ' \ и перевода строки бэкслэшем — quote(строка).
Экранирование символа " удвоением - sql_quote(строка). (актуально также для CSV)
Экранирование символов, являющихся специальными в регулярных выражениях — re_quote(строка). (см. perldoc perlre).
URI_QUOTE=URIQUOTE=URLENCODE
URL-кодирование строки (URI::Escape в Perl и urlencode() в PHP).
REPLACE, STR_REPLACE
Замена Perl- (соответственно PCRE- в PHP-версии) регулярного выражения в строке — replace(RegExp, замена, строка).
Замена подстроки в строке - str_replace(искомое, замена, строка).
STRLEN
Длина строки в символах.
SUBSTR=SUBSTRING
Стандартная (для всех, кроме жавистов) функция подстроки — substr(строка, начало, длина), или substr(строка, начало). Причём начало и длина могут быть отрицательными, тогда они считаются относительно длины строки.
TRIM
Удаление пробелов из начала и конца строки.
SPLIT
Разделение строки по регулярному выражению и лимиту — split(RegExp, аргумент, лимит). Лимит необязателен. (см. perldoc -f split)
S=HTML=HTMLSPECIALCHARS, T=STRIP, H=STRIP_UNSAFE, NL2BR
Преобразование символов < > & " ' в HTML-сущности.
Удаление только «небезопасных» HTML-тегов.
Преобразование переводов строк (\n) в HTML-тег <br />.
CONCAT, JOIN=IMPLODE
Конкатенация всех своих аргументов — concat(аргументы). Конкатенирует также все элементы всех переданных массивов.
Конкатенация элементов массива через разделитель — join(строка, аргументы). Конкатенирует также все элементы всех переданных массивов.
SUBST, SPRINTF, STRFTIME
Подстановка на места подстрок вида $ЧИСЛО соответствующих параметров функции или элементов переданного массива — subst(строка, $1, $2, …).
Sprintf — он и в Африке sprintf.
Форматирование даты и/или времени с помощью функции strftime — strftime(формат, дата [, часть_даты]). Формат strftime’овский (например, «%d %b %Y»). Дата может передаваться как один или два аргумента, если два — они конкатенируются через пробел. Далее дата разбирается способом, похожим на wfTimestamp() в MediaWiki. Принимается следующее:
- UNIX время.
- Времена типа MySQL DATE, MySQL DATETIME, EXIF, ISO 8601, MediaWiki, и любые другие, подпадающие под следующий формат: 1 группа из 4 или более цифр (год) и 2 (месяц, день) или 5 (месяц, день, часы, минуты, секунды) групп по 2 цифры, разделённые любыми нецифровыми символами и в конце — опционально временная зона — 2 цифры, предварённые пробелом, плюсом или минусом. Короче говоря,
^\D*(\d{4,})\D*(\d{2})\D*(\d{2})\D*(?:(\d{2})\D*(\d{2})\D*(\d{2})\D*([\+\- ]\d{2}\D*)?)?$
- Оракловский формат даты-времени: ДД-Мес-ГГ[ГГ] ЧЧ.ММ.СС.
- RFC 822.
STRLIMIT
Ограничение длины строки s максимальной длиной l — strlimit(s, l). Если строка превышает заданную длину, она обрезается предпочтительно по пробелу или Tab’у, а в конец добавляется «…» (троеточие).
Массивы и хеши
HASH
Создание хэша из всех аргументов.
Соответственно в хеше аргументы идут парами КЛЮЧ, ЗНАЧЕНИЕ, КЛЮЧ, ЗНАЧЕНИЕ и т. п. (специального синтаксиса «=>» нет).
KEYS, HASH_KEYS, ARRAY_KEYS
Массив ключей хэша. Понятное дело, в PHP их порядок сохраняется, а в Perl — нет.
SORT
Сортировка массива по значениям.
EACH
Массив хэшей вида { id => ключ, name => значение } для хэша, в случае Perl ключи будут отсортированы по имени.
ARRAY, RANGE
Создание массива.
Диапазон от A до B — range(A, B).
IS_ARRAY
Проверка, является ли аргумент массивом. В PHP-версии не проверяется, а не является ли он при этом хэшем, ибо трудоёмко.
COUNT, SUBARRAY=ARRAY_SLICE, SUBARRAY_DIVMOD
Количество элементов массива, или 0, если аргумент — не массив — count(аргумент).
Аналог функции array_slice из PHP.
Выбор из массива каждого div’того элемента, начиная с номера mod или нуля по умолчанию — subarray_divmod(массив, div, mod).
GET, AGET, HGET
Получение элемента массива/хэша по «динамическому» ключу. По-моему, это лучше, чем зюки-хрюки Template Toolkit’а: hash.${hash2.$key} и т. п.
GET(откуда, что) автоматически решает, «откуда» — это массив или хеш, AGET служит только для массивов, а HGET только для хешей. В PHP-версии все три идентичны.
GET(что) — получение значения переменной верхнего уровня.
ARRAY_MERGE
Слить массивы в один. Под Perl — только массивы (не хеши), под PHP — любые массивы.
SHIFT, POP, UNSHIFT, PUSH
Вынуть элемент из начала массива, вынуть из конца, добавить в начало — unshift(array, value), добавить в конец — push(array, value).
Прочее
VOID
Вычислить аргумент и вернуть пустую строку. Потенциально нужно для игнорирования результата, ибо все возвращаемые значения радостно подставляются в выходной поток.
DUMP=VAR_DUMP
Вывод всех данных из структуры — Dumper в Perl’е и var_dump в PHP.
JSON
Форматирование любой структуры данных в формат JSON.
INCLUDE=PROCESS=PARSE
Включение другого шаблона или выполнение блока.
process('имя файла') process('имя файла', 'имя блока') process('имя файла', 'имя блока', hash( аргументы )) process('::имя блока в текущем шаблоне' [, hash(аргументы)])
Не рекомендуется, но возможно также и передавать код вместо имени файла:
process('', 'код шаблона' [, 'функция'] [, hash( аргументы )])
CALL
Вызов метода объекта по «динамическому» имени — call(varref, method_name, arg1, arg2, arg3, ...).
MAP
Применение функции, имя которой передано как первый аргумент, ко всем переданным аргументам и элементам всех переданных массивов — map(«имя_функции», аргументы).