Snakeskin — это язык шаблонов и движок для их трансляции в JavaScript. Мощный набор возможностей и развитая защита от уязвимостей типа XSS позволяют полностью сосредоточиться на представлении данных, делая его независимым от бизнес-логики. Таким образом, Snakeskin идеально подходит для ситуаций, когда бэкенд и фронтенд разработчики работают параллельно.
Благодаря тому, что шаблоны Snakeskin транслируются в JS-код стандарта ECMAScript 5, независимый от окружения, он одинаково хорошо подходит для использования как на стороне сервера, так и на клиенте. Это можно сравнить с принципом работы CoffeeScript, но если CoffeeScript спроектирован для упрощения разработки сложных проектов непосредственно на JS, то Snakeskin предназначен для шаблонизации HTML/XML-подобных структур и упрощения их code-reuse, предоставляя из коробки наследование, расширение, локализацию, экранирование и прочие полезные ништяки :)
Snakeskin удобно использовать для генерации статичных страниц — транслятор может скомпилировать и сразу выполнить шаблон, возвратив результат его работы, что можно использовать для генерации шаблонов различных MVVM-фреймворков, таких как Angular, Vue или React. В принципе, можно использовать Snakeskin даже для генерации кода на PHP — но помните, мы не гарантируем защиты от динозавров, которые могут прийти за вами.
Snakeskin поддерживает интеграцию с популярными task-runner'ами и сборщиками (Grunt, Gulp, Webpack) с помощью плагинов — это позволяет бесшовно встроить его в систему сборки любого проекта.
Use and enjoy.
Snakeskin написан на языке JavaScript и поддерживает работу как в браузере, так и в других окружениях:
bower install --save snakeskin
Файлы библиотеки лежат по адресу:
bower_components/
snakeskin/
dist/
snakeskin.min.js
snakeskin.live.min.js
npm install --save snakeskin
std.ss — это библиотека написанная на Snakeskin, которая предоставляет ряд дополнительных функций для разработки.
git clone https://github.com/SnakeskinTpl/Snakeskin
Шаблоны в Snakeskin — это функции в JavaScript.
- namespace demo
- template helloWorld(name = 'world')
Hello {name}!
{namespace demo}
{template helloWorld(name = 'world')}
Hello {name}!
{/template}
Эквивалентно
if (exports.demo === 'undefined') {
var demo = exports.demo = {};
}
exports.demo.helloWorld = function helloWorld(name) {
return 'Hello ' + escape(name) + '!';
}
После компиляции вызов шаблона соответствует простому вызову функции. Такой же подход используется в Google Closure Templates.
Основные use-case Snakeskin — это:
Также есть возможность «живой» компиляции в браузере, хотя данная фича не рекомендуется для prod.
Простейшим способом изучения возможностей Snakeskin является использование онлайн-песочницы на Codepen.
Snakeskin содержит ряд вспомогательных методов для удобной работы из Node.js.
templates.ss
- namespace registration
- template index()
< input name = login | type = text | placeholder = Логин
< input name = password | type = password | placeholder = Пароль
{namespace registration}
{template index()}
{< input name = login | type = text | placeholder = Логин /}
{< input name = password | type = password | placeholder = Пароль /}
{/template}
index.js
var ss = require('snakeskin');
console.log(
ss.compileFile('./templates.ss').registration.index()
);
Скомпилированные файлы Snakeskin подключаются как простые Node.js модули.
index.js
var tpls = require('./templates.ss.js');
Snakeskin бесшовно интегрируется с большинством популярных MVVM библиотек и фреймворков, а использование SS вместе с Webpack является одной из самых лучших практик.
webpack.config.js
var webpack = require('webpack');
webpack({
entry: {
index: './button.js'
},
output: {
filename: '[name].bundle.js'
},
module: {
loaders: [
{
test: /.js$/,
exclude: /node_modules/,
loader: 'babel'
},
{
test: /.ss$/,
exclude: /node_modules/,
loader: 'snakeskin-loader?pack=true'
}
]
}
});
button.ss
- namespace button
- template index()
< button.b-button :type = type | :form = form
{{ label }}
{namespace button}
{template index()}
{< button.b-button :type = type | :form = form}
{{ label }}
{/}
{/template}
button.js
import { button } from './button.ss';
Vue.component('button', {
props: {
label: {
type: String,
required: true
},
type: String,
form: String
},
template: button.index()
});
В примере используется JS стандарта ES2015, поэтому также добавлен лоадер для транслятора Babel, чтобы обеспечить поддержку в старых браузерах.
Snakeskin может немедленно выполнить скомпилированный шаблон по заданному имени и вернуть результат, а если имя шаблона не задано, то оно будет вычислено по формуле:
шаблон с именем главного файла (без расширения) ||
main ||
index ||
первый шаблон в списке
gulpfile.js
var
gulp = require('gulp'),
ss = require('gulp-snakeskin');
gulp.task('snakeskin', function () {
gulp.src('index.ss')
.pipe(ss({exec: true}))
.pipe(gulp.dest('./dist/index.html'));
});
gulp.task('default', ['snakeskin']);
index.ss
- namespace index
- template main()
< .hello
Hello world!
{namespace index}
{template main()}
{< .hello}
Hello world!
{/}
{/template}
Также для задач статической генерации страниц может использоваться Grunt, Webpack или Snakeskin CLI.
Генерация страницы index.html на основе шаблона index.ss из примера выше.
snakeskin ./index.ss -e -o ./dist/index.html
Шаблоны Snakeskin можно декларировать либо в отдельных файлах c расширением .ss (рекомендуемый способ), либо внутри HTML документа через <script>
блок, например:
<!doctype html>
<html>
<head>
<title>Hello world!</title>
<meta charset="utf-8">
</head>
<body>
<script type="text/x-snakeskin-template">
{namespace demo}
{template index()}
Hello world!
{/template}
{template calc(a, b)}
a + b = {a + b}
{/template}
</script>
</body>
</html>
В одной области декларации может быть объявлено неограниченное количество шаблонов.
Управляющие конструкции (директивы) Snakeskin размещаются между символами {
и }
. Обычно вызов директивы подходит под следующий шаблон:
{названиеДирективы параметры}
Например:
{var a = true /}
{if a}
{void console.log(a)}
{/}
Но некоторые директивы поддерживают альтернативный, более короткий синтаксис, например:
{void 1 + 2} ↔ {? 1 + 2}
{output 1 + 2} ↔ {1 + 2}
В случаях, когда в теле генерируемого шаблона необходимо также использовать фигурные скобки, то используется специальный расширенный синтаксис декларации директив (символы #{
и }
), например:
{template example()}
{var val = 1 /}
#{block script}
<script>
var a = {
val: #{val}
};
</script>
#{/}
{/template}
Все директивы, которые вложены в директиву с расширенным синтаксисом, должны также использовать этот синтаксис.
Ряд директив Snakeskin поддерживают специальный синтаксис интерполяции значений, который нужен для проброса динамических выражений в статический текст через конструкцию ${выражение}
.
{var a = true /}
{tag ${a ? 'input' : 'textarea'} /}
Подобно тегам HTML или XML, многие директивы Snakeskin могут включать в себя другие директивы и т. д. и в дальнейшем такие директивы будут называться блочными, а все остальные — строчными.
Блочные директивы состоят из двух частей: основной декларации и завершающей части, например:
/// Основная декларация директивы if
{if 1 > 2}
...
/// Завершающая часть
{/}
Завершающая часть, как правило, создаётся с помощью специальной директивы end, которая поддерживает несколько видов синтаксиса:
/// Полная форма
{if 1 > 2}
...
{end if}
/// Сокращённая форма
{if 1 > 2}
...
{end}
/// Альтернативная полная форма
{if 1 > 2}
...
{/if}
/// Альтернативная сокращённая форма
{if 1 > 2}
...
{/}
Какую форму использовать решает сам разработчик, но следует отметить, что при использовании форм с указанием имени закрываемой директивы Snakeskin будет проверять правильность, т. е.:
{if 1 > 2}
...
{/else} /// Ошибка
В любом месте области декларации шаблонов допускается использовать однострочные (///
) и многострочные (/* ... */
) комментарии. Комментарии вырезаются на этапе трансляции и не попадают в скомпилированный JavaScript.
/* ... */
/// ...
{template index(name)}
///{name}
/*Hello
world*/
{1 /*+ 2*/} /// Выведет 1
/// /* 1 */, т.к. внутри литералов строк и
/// регулярных выражений комментарии не действуют
{'/* 1 */'}
/* Для отмены нежелательного комментария его нужно экранировать */
file:\///... /// экранируем первый /, чтобы URL вывелся как надо
/// Пример ниже вызовет ошибку
{/*}*/
{/template}
Snakeskin поддерживает jsDoc комментарии, которые не вырезаются из конечного JavaScript кода.
/**
* jsDoc комментарий
* @param {string} name
*/
{template index(name)}
/*
* Обычный комментарий
*/
{/template}
Символ \
может использоваться для экранирования директив, комментариев и прочих сущностей Snakeskin.
{template example()}
\{var val = 1} /// Выведется как простой текст
\/// Выведется как простой текст
\\1 /// \1
{/template}
Помимо основного синтаксиса Snakeskin поддерживает также альтернативный, который основан на принципе управляющих пробелов и по духу близок к Jade и HAML. Такой синтаксис крайне удобен при генерации XML подобных структур, например:
- template hello(name)
< h1.foo
< span style = color: red
Hello {name}!
Вместо символов {
, }
для вывода директивы используется -
. В примерах документации основной синтаксис будет называться classic, а альтернативный — jade-like.
«Классический» синтаксис:
{namespace demo}
{template index(name)}
{if name}
{name}
{/}
{/template}
Jade-like синтаксис:
- namespace demo
- template index(name)
- if name
{name}
Следует заметить, что в jade-like синтаксисе нет необходимости ставить закрывающие теги для блочных директив, т. к. они проставляются автоматически.
Если директива имеет короткий синтаксис, то тогда управляющий символ -
может быть опущен, например:
/// - var foo = 'bar'
: foo = 'bar'
/// - void foo = 'bar'
? console.log(foo)
Для использования расширенного синтаксиса используется символ #
вместо -
,
причём управляющий символ должен писаться всегда (в том числе и для короткого синтаксиса директив).
: foo = true
# if true
#? console.log(foo)
Jade-like синтаксис можно использовать совместно с классическим, при этом классический синтаксис может включаться в блоки с декларацией jade-like, например:
/// Альтернативный синтаксис
- namespace demo
/// Классический синтаксис
{template index()}
....
{/template}
/// Альтерантивный синтаксис с включением классического
- template wrapper(name)
Hello {name}!
В случае, если необходимо разбить тело директивы на несколько строк, то используется конструкция & ... .
(перед символами необходимо ставить пробел), например:
- template hello(name)
< h1.foo &
style =
color: red;
text-decoration: underline
.
{name}
Содержимое директивы можно задать на одной строке с декларацией после символов ::
(обязательное выделение пробелами с обеих сторон; после спецсимвола можно использовать только классический синтаксис).
- template hello(name)
< h1.foo :: {name}
Однако, при многострочной декларации директивы такой синтаксис невозможен.
Специальный символ |
в начале строки декларирует, что вся строка является простым текстом (внутри которого по прежнему можно использовать классический синтаксис), а сам символ игнорируется, например:
- template hello(name)
- if true :: Hello world! /// Hello world!
|- if true :: Hello world! /// - if true :: Hello world!
Следует отметить, что также для этой цели можно использовать универсальный символ экранирования \
:
- template hello(name)
\- if true :: Hello world! /// - if true :: Hello world!
Т.е. можно использовать любой из способов.
Шаблон — это главная функциональная ячейка Snakeskin, которая является синонимом функции в JavaScript, т. е. после трансляции все шаблоны будут представлены как JS функции, которые можно использовать вместе с любым другим JS кодом.
Объявление шаблона возможно с помощью директив template, interface и placeholder. Шаблон не может включать в себя другой шаблон, для этого есть другие директивы.
Название шаблона соответствует названию функции в JavaScript, поэтому оно подчиняется тем же правилам.
Все шаблоны Snakeskin декларируются в пространстве имён, которое обязан задать сам разработчик с помощью директивы namespace, причём в рамках одного файла Snakeskin может существовать только одно пространство имён, т. е.:
- namespace demo
- namespace bar /// Ошибка
{namespace demo}
{namespace bar} /// Ошибка
Каждое пространство имён является свойством корневого объекта exports, которое является глобальным для всех файлов Snakeskin и осуществляет экспорт шаблонов в JavaScript.
В рамках одного пространства имён не может быть двух шаблонов с одинаковым названием, т. е. переопределять шаблоны в Snakeskin нельзя.
Snakeskin в известном смысле — макроязык, в нём нет оператора print, т. е. весь текст, набранный в исходном файле, суть большой оператор print. Директивы Snakeskin являются погруженными в текст. Получается, что вы не пишете программу, которая выводит текст — наоборот, в имеющийся текст вы добавляете логику и организацию, блоки (методы), на которые вы разбиваете код:
- namespace demo
- template hello()
Hello world!
{namespace demo}
{template hello()}
Hello world!
{/template}
В JS такое будет выглядеть как:
if (exports.demo === 'undefined') {
var demo = exports.demo = {};
}
exports.demo.hello = function hello() {
return 'Hello world!';
};
Snakeskin позволяет разработчику вставлять в статичный текст шаблона динамические JavaScript выражения, например, вызовы функций, значения переменных, результат математических операций и т. д. Для этого используется специальная директива output. Давайте усложним пример данный выше: будем выводить приветствие по заданному параметру-имени.
- namespace demo
- template hello(name)
/// Можно было бы написать {output name},
/// но внутри шаблонов директива output
/// имеет более удобную короткую форму вызова и поэтому
/// достаточно просто взять выводимое выражение в фигурные скобки
Hello {name}!
{namespace demo}
{template hello(name)}
/// Можно было бы написать {output name},
/// но внутри шаблонов директива output
/// имеет более удобную короткую форму вызова и поэтому
/// достаточно просто взять выводимое выражение в фигурные скобки
Hello {name}!
{/template}
Как уже говорилось выше, выводить через output можно не только простые параметры, а практически любое JavaScript выражение, например:
- namespace demo
- template calc(a, b)
a + b = {Math.round(a) + b}
{a > 10 ? '"a" great then 10' : '"a" less then 10'}
{namespace demo}
{template calc(name)}
a + b = {Math.round(a) + b}
{a > 10 ? '"a" great then 10' : '"a" less then 10'}
{/template}
Помимо output, в Snakeskin существует множество других директив, которые помогут написать функциональный и гибкий шаблон, например:
- namespace demo
- template index(value)
- if Array.isArray(value)
- forEach value => el
{el}
{namespace demo}
{template index(name)}
{if Array.isArray(value)}
{forEach value => el}
{el}
{/}
{/}
{/template}
Узнать о всех директивах, которые поддерживает Snakeskin, можно в документации проекта.
Код, который находится вне тела шаблона или прототипа считается глобальным и помимо трансляции в JavaScript выполняется на этапе трансляции.
/// Данный forEach выполнится на этапе трансляции и войдёт в конечный JS
- forEach [1, 2, 3] => el
? console.log(el)
/// Данный forEach выполнится на этапе трансляции и войдёт в конечный JS
{forEach [1, 2, 3] => el}
{? console.log(el)}
{/}
Если не задан параметр tolerateWhitespaces, то любые пробельные символы (перевод строки, пробел, табуляция и т. д.) в рамках шаблона Snakeskin трактуются как пробел и «схлопываются» в один, т. е.:
- namespace demo
- template index()
Hello world
Bar
{namespace demo}
{template index()}
Hello world
Bar
{/template}
Отрендерится как:
Hello world Bar
Исключение составляют блоки cdata, литералы строк и регулярных выражений внутри директивы и jsDoc комментарии.
С помощью параметра ignore можно задать те пробельные символы, которые будут полностью вырезаться из шаблона.
Директивы Snakeskin можно условно разделить на 2 группы: текстовые и логические.
Текстовые директивы — это такие директивы, результат работы которых выводится в шаблон, например, output или call, а остальные директивы считаются логическими, например, if или for.
Логические директивы не участвуют в обработке пробелов, т. е.:
- namespace demo
- template index()
Hello
- if true
- if true
world
{namespace demo}
{template index()}
Hello
{if true}
{if true}
world
{/if}
{/if}
{/template}
Отрендерится как:
Hello world
Функциональные директивы Snakeskin — это такие директивы, которые по своему синтаксису напоминают функции JavaScript, и как правило при трансляции преобразуются именно в них. Логично, что такие директивы могут принимать входные параметры, например:
- namespace demo
- template index(a, b)
{a + b}
{namespace demo}
{template index(a, b)}
{a + b}
{/template}
У параметров может быть значение по умолчанию, которое применяется, если значение параметра будет null
или undefined
.
- namespace demo
- template index(a = 1, b = 2)
{a + b}
{namespace demo}
{template index(a = 1, b = 2)}
{a + b}
{/template}
С помощью специального оператора ?
можно задать, чтобы значение по умолчанию ставилось только при undefined
.
- namespace demo
- template index(a? = 1, b? = 2)
{a + b}
{namespace demo}
{template index(a? = 1, b? = 2)}
{a + b}
{/template}
Существует также оператор !
, который отменяет оператор ?
, но он используется только при наследовании блоков или шаблонов.
Оператор @
осуществляет привязку параметра к директиве with, например:
- namespace demo
- template index(@params)
{@name} /// params.name
{namespace demo}
{template index(@params)}
{@name} /// params.name
{/template}
Фильтром Snakeskin называется функция, которая лежит в пространстве имён Snakeskin.Filters, а для вызова такой функции используется специальный «сахарный» синтаксис. Сам механизм фильтров идеологически близок к реализации
в Bash, т. е. для вызова фильтра используется символ вертикальной черты (|
), после которого идёт название фильтра и его параметры. Следует отметить, что между символом фильтра и названием не должно быть пробелов, иначе
это будет трактоваться парсером как «побитовое или».
Например:
/// Snakeskin.Filters['ucfirst'].call(this, 'hello' + ' world')
{'hello' + ' world'|ucfirst}
Как и в Bash, фильтры Snakeskin могут создавать конвейерные последовательности:
{' hello '|trim|ucfirst}
Фильтры можно накладывать отдельно на некоторые части выражения, для этого нужно обернуть декларацию в круглые скобки:
/// Два локальных и один глобальный фильтр,
/// который применится на всё выражение
{(a|ucfirst) + (b|ucfirst) |trim}
Фильтрам можно передавать параметры, на которые также можно применять фильтры и т. д.
{a|myFilter 1, (2 / 3|myFilter)}
По умолчанию при выводе значений через output к ним применяется глобальный фильтр html (экранирование html символов) и фильтр undef (замена undefined
на пустые строки), однако их выполнение можно
отменить {a|!html}
и {a|!undef}
.
Механизм фильтров поддерживает большинство директив Snakeskin, например:
- var a = 'fooo '|trim
- if a|trim
...
{var a = 'fooo '|trim /}
{if a|trim}
...
{/}
Фильтры Snakeskin можно назначать на любые входные параметры функциональных директив ( template, forEach и т. д.), например:
- template demo((a|trim), (b|trim|remove 'foo'))
{template demo((a|trim), (b|trim|remove 'foo'))}
{/template}
Обратите внимание, что фильтр параметра берётся в круглые скобки.
На значения по умолчанию для параметров можно также накладывать фильтры, причём если у самого параметра есть свои фильтры, то они применятся после, например:
- template demo((a|trim) = ('fooBar'|remove 'foo'))
{template demo((a|trim) = ('fooBar'|remove 'foo')}
{/template}
Чтобы написать свой фильтр, достаточно добавить его в Snakeskin.Filters. Название фильтра может начинаться с символа латинского алфавита, подчёркивания (_
) или знака доллара ($
). Первым параметром
функции будет значение выражения, а this внутри фильтра ссылается на this шаблона.
Snakeskin.Filters['replace'] = function (str, search, replace) {
return String(str).replace(search, replace);
};
Фильтры можно разбивать на внутренние пространства имён:
Snakeskin.Filters['text'] = {
'replace': function (str, search, replace) {
return String(str).replace(search, replace);
}
};
{'foo'|text.replace 'fo', 'bar'}
Также для добавления своих фильтров можно воспользоваться методом Snakeskin.importFilters:
Snakeskin.importFilters({
'replace': function (str, search, replace) {
return String(str).replace(search, replace);
}
});
// С указанием пространства имён
// my.foo.bar.repeat
Snakeskin.importFilters({
'replace': function (str, search, replace) {
return String(str).replace(search, replace);
}
}, 'my.foo.bar');
При декларации фильтра ему можно задать ряд дополнительных параметров, например, чтобы фильтр отменял выполнение фильтров по умолчанию. Для этого используется специальный метод Snakeskin.setFilterParams, который первым параметром принимает название фильтра, а вторым объект с настройками.
Snakeskin.importFilters({
myFilter: function (str, getTplResult) {
return str + getTplResult();
}
});
Snakeskin.setFilterParams('myFilter', {
// Прокидываем функцию getTplResult и объект $attrs из шаблона в фильтр
bind: ['getTplResult', function (o) { return o.getVar('$attrs'); }],
// Отменяем фильтр html по умолчанию
'!html': true,
// Отменяем фильтр undef по умолчанию
'!undef': true,
// Указывает, что фильтр не меняет исходное значение так,
// что это может привести к появлению XSS, т.е. фильтр безопасный,
// например таким фильтром является trim
'safe': true
});
Обратите внимание, что такие параметры должны задаваться до трансляции шаблона.
Переменная $_ содержит результат последней работы фильтра.
{' fooo '|trim}
{$_} /// 'fooo'
Чтобы использовать в шаблоне побитовое ИЛИ (|
) достаточно просто указать пробел после оператора |
, также если после оператора |
идёт число, то можно писать как есть, т. к. название фильтра не может
начинаться с числа.
{1|0}
{a = 1}
{1 | a}
В стандартном runtime библиотеки Snakeskin присутствует ряд базовых фильтров, которые могут быть полезны при разработке шаблонов, подробнее.
В Snakeskin существует специальный тип строк ` ... `
, которые при параметре
localization (включён по умолчанию) будут автоматически оборачиваться функцией i18n()
.
- namespace demo
- template index()
`Hello world!`
{namespace demo}
{template index()}
`Hello world!`
{/template}
Строка `Hello world!`
скомпилируется как
i18n("hello world!")
Данные строки можно также использовать внутри директив, например:
- namespace demo
- template index()
{`Hello world!`}
{namespace demo}
{template index()}
{`Hello world!`}
{/template}
Имя используемой функции локализации можно задать явно с помощью параметра i18nFn.
Snakeskin поддерживает передачу параметров для функции локализации, которые задаются в виде строки через параметр i18nFnOptions, например:
- namespace demo
- template index()
`Hello world!`
{namespace demo}
{template index()}
`Hello world!`
{/template}
Snakeskin.compile('<шаблон>', {
i18nFnOptions: '{lang: "en"}, true'
});
Строка локализации скомпилируется как
i18n("hello world!", {lang: "en"}, true)
Если строка локализации находится внутри директивы, то можно явно передать дополнительные параметры, которые будут заменять глобальные, например:
- namespace demo
- template index()
{`Привет мир!`({lang: "ru"}, true)}
{namespace demo}
{template index()}
{`Привет мир!`({lang: "ru"}, true)}
{/template}
Чтобы использовать символ `
самостоятельно, то его нужно экранировать.
- namespace demo
- template index()
\`Hello world!\`
{namespace demo}
{template index()}
\`Hello world!\`
{/template}
Отрендерится как:
`Hello world!`
Или же можно просто отключить параметр localization.
Если при компиляции шаблонов задать параметр-объект language
, то найденные литералы будут автоматически заменятся на указанные в объекте.
- namespace demo
- template index()
`Hello world!`
{namespace demo}
{template index()}
`Hello world!`
{/template}
Snakeskin.compile('<шаблон>', {
language: {
// В качестве значений замены можно также использовать функции,
// которые возвращают строку
'Hello world!': 'Привет мир!'
}
});
Отрендерится как:
Привет мир!
Т.к. смежные пробельные символы в Snakeskin схлопываются один, то следующий пример также будет работать.
- namespace demo
- template index()
`Hello
world!`
{namespace demo}
{template index()}
`Hello
world!`
{/template}
Snakeskin.compile('<шаблон>', {
language: {
'Hello world!': 'Привет мир!'
}
});
Дополнительно Snakeskin может сохранить все найденные литералы локализации в объект, чтобы потом, например, можно было передать его переводчикам. За эту функцию отвечает параметр words.
- namespace demo
- template index()
`Hello world!`
{namespace demo}
{template index()}
`Hello world!`
{/template}
var words = {};
Snakeskin.compile('<шаблон>', {
words: words
});
// {'Hello world!': 'Hello world!'}
console.log(words);
Транслятор Snakeskin поддерживает множество дополнительных режимов работы, которые можно задавать через метод compile и т. д., но также, Snakeskin позволяет декларировать часть таких параметров непосредственно в тексте программы, например:
@= tolerateWhitespaces true
@= literalBounds ['<?php', '?>']
{@= tolerateWhitespaces true}
{@= literalBounds ['<?php', '?>']}
Задачу параметров трансляции выполняет директива set, которая имеет короткий синтаксис декларации @=
. Поддерживаются следующие параметры:
Параметры заданные таким способом распространяются на все шаблоны в файле, а также на все подключаемые файлы, если они там явно не переопределяются.
Параметр трансляции можно задать локально для конкретного шаблона, причём можно совмещать оба способа декларации, например:
@= tolerateWhitespaces true
- namespace demo
- template index() @= literalBounds ['<?php', '?>']
{{ Hello }}
{@= tolerateWhitespaces true}
{namespace demo}
{template index() @= literalBounds ['<?php', '?>']}
{{ Hello }}
{/template}
Snakeskin поддерживает бесшовную интеграцию с другими шаблонными движками с помощью специальной директивы literal. Как правило такой кейз возникает при интеграции Snakeskin c MVVM фреймворками, например,
с Vue. Директива literal имеет специальный синтаксис {{ ... }}
, который скомпилируется согласно параметру literalBounds (по умолчанию используется {{ ... }}
),
например:
- namespace demo
- template index() @= literalBounds ['<?php', '?>']
{{ Hello }}
{namespace demo}
{template index() @= literalBounds ['<?php', '?>']}
{{ Hello }}
{/template}
Отрендерится как:
<?php Hello ?>
Для передачи значений Snakeskin внутрь директивы используется стандартный механизм интерполяции, например:
- namespace demo
- template index()
{{ bar${1 + 2} }}
{namespace demo}
{template index()}
{{ bar${1 + 2} }}
{/template}
Отрендерится как:
{{ bar3 }}
Важно понимать отличие простой декларации шаблона от декларации шаблона с наследованием. При декларации простого шаблона мы описываем его структуру, а при наследовании описываем его изменения относительно родителя, например:
- namespace demo
- template base()
Какой хороший день!
- template child() extends @base
Может пойдём на речку?
{namespace demo}
{template base()}
Какой хороший день!
{/template}
{template child() extends @base}
Может пойдём на речку?
{/template}
При вызове скомпилированной функции child результат будет точно таким же, как и у base: Какой хороший день!
, но мы скорее всего ожидали увидеть Какой хороший день! Может пойдём на речку?
.
Всё дело в том, что в тексте шаблона child не были указаны изменения, а просто написана некоторая новая структура и Snakeskin не знает как именно она должна расширять родительский шаблон и поэтому проигнорировал её.
Одним из способов декларации переопределяемых структур являются блоки (вызываемые блоки являются частным случаем), они позволяют создавать специальные структурные пометки, например:
- namespace demo
- template base()
- block root
Какой хороший день!
- template child() extends @base
- block root
Может пойдём на речку?
{namespace demo}
{template base()}
{block root}
Какой хороший день!
{/}
{/template}
{template child() extends @base}
{block root}
Может пойдём на речку?
{/}
{/template}
Теперь при вызове child результат будет: Может пойдём на речку?
, т. к. в родительском шаблоне мы декларировали блок root, а в дочернем переопределили его. Мы также можем расширять дочерний шаблон путём
введения новых блоков, которых нет у родителя, и тогда они будут последовательно подставляться в конец тела родителя.
- namespace demo
- template base()
Какой хороший день!
- template child() extends @base
- block sub
Может пойдём на речку?
{namespace demo}
{template base()}
Какой хороший день!
{/template}
{template child() extends @base}
{block sub}
Может пойдём на речку?
{/}
{/template}
Теперь результат child: Какой хороший день! Может пойдём на речку?
.
Сами по себе блоки никак не влияют на конечный вид шаблона, т. е.
- namespace demo
- template base()
- block root
Какой хороший день!
- template base2()
Какой хороший день!
{namespace demo}
{template base()}
{block root}Какой хороший день!{/}
{/template}
{template base2()}
Какой хороший день!
{/template}
Оба шаблона дадут абсолютно одинаковый ответ.
В пределах шаблона не может быть двух блоков с одинаковым названием. Название блоков подчиняется тем же правилам, что и идентификаторы в JS. Внутри блока можно декларировать другие директивы, в частности другие блоки, константы и т. д.
- namespace demo
- template base()
- block base
Какой хороший
- block sub
день
!
- template child() extends @base
- block sub
пень
{namespace demo}
{template base()}
{block base}
Какой хороший {block e}день{/}!
{/}
{/template}
{template child() extends @base}
{block sub}пень{/}
{/template}
Если нам нужно доопределить родительский блок, то для этого следует использовать директиву super. Директива работает по схеме: всплывает по дереву шаблона до тех пор, пока не найдётся блок, который имеет родителя, и вставляет родительское тело в указанное место, а если такого родителя нет, то просто ничего не делает.
- namespace demo
- template base()
- block base
Какой хороший день!
- template child() extends @base
- block base
- super
Трудиться мне не лень!
{namespace demo}
{template base()}
{block base}
Какой хороший день!
{/}
{/template}
{template child() extends @base}
{block base}
{super}
Трудиться мне не лень!
{/}
{/template}
При наследовании шаблон или блок наследует входные параметры родителя только в случае явного перечисления, при этом если входной параметр имеет у родителя значение по умолчанию, а у потомка оно не указано, то оно наследуется также.
- namespace demo
- template base(a = 1, b)
/// Шаблон имеет один параметр "a" со значением по умолчанию 1
- template child(a) extends @base
/// Шаблон имеет один параметр "a" со значением по умолчанию 2
- template child2(a = 2) extends @base
{namespace demo}
{template base(a = 1, b)}
{/template}
/// Шаблон имеет один параметр "a" со значением по умолчанию 1
{template child(a) extends @base}
{/template}
/// Шаблон имеет один параметр "a" со значением по умолчанию 2
{template child2(a = 2) extends @base}
{/template}
Наследуется и отношения параметра к null
— ?
и !
, причём в дочернем шаблоне можно переопределить поведение, например:
- namespace demo
- template base(a? = 1)
- template child(a!) extends @base
{namespace demo}
{template base(a? = 1)}
{/template}
{template child(a!) extends @base}
{/template}
Фильтры параметров также наследуются, причём мы можем добавлять новые фильтры в дочернем шаблоне (они будут применятся после родительских).
- namespace demo
- template base((a|trim))
/// a|trim|ucfirst
- template child((a|ucfirst)) extends @base
{namespace demo}
{template base((a|trim))}
{/template}
/// a|trim|ucfirst
{template child((a|ucfirst)) extends @base}
{/template}
Параметры имеют привязку по своему названию, а не порядку.
- namespace demo
- template base(a = 1, b = 2)
/// b = 2, a = 1
- template child(b, a) extends @base
{namespace demo}
{template base(a = 1, b = 2)}
{/template}
/// b = 2, a = 1
{template child(b, a) extends @base}
{/template}
В случае если потомок исключает родительский параметр, то он становится простой локальной переменной внутри шаблона.
- namespace demo
- template base(a = 1)
/// Параметр "a" стал переменной со значением a = 1
- template child() extends @base
{namespace demo}
{template base(a = 1)}
{/template}
/// Параметр "a" стал переменной со значением a = 1
{template child() extends @base}
{/template}
Помимо исключения родительских параметров в дочернем шаблоне допускается и добавление новых.
- namespace demo
- template base(a = 1)
- template child(b = 3, a) extends @base
{namespace demo}
{template base(a = 1)}
{/template}
{template child(b = 3, a) extends @base}
{/template}
with биндинг параметров также наследуется, если он не был переопределён явно.
- namespace demo
- template helloWorld(@params = {name: 'friend'})
< h1
Hello {@name}!
/// params наследуется как @params
- template helloWorld2(params) extends @helloWorld
{namespace demo}
{template helloWorld(@params = {name: 'friend'})}
<h1>Hello {@name}!</h1>
{/template}
/// params наследуется как @params
{template helloWorld2(params) extends @helloWorld}
{/template}
При наследовании шаблона также наследуются те параметры трансляции, которые были ассоциированы с родительским шаблоном.
- namespace demo
- template base(data) @= renderMode 'dom'
/// @= renderMode 'dom'
- template child(data) extends @base
{namespace demo}
{template base(data) @= inlineIterators true @= renderMode 'dom'}
{/template}
/// @= renderMode 'dom'
{template child(data) extends @base}
{/template}
Допускается переопределение или доопределение параметров трансляции в дочернем шаблоне.
- namespace demo
- template base(data) @= renderMode 'stringConcat'
- template child(data) extends @base @= renderMode 'dom' @= tolerateWhitespaces true
{namespace demo}
{template base(data) @= inlineIterators true @= renderMode 'stringConcat'}
{/template}
{template child(data) extends @base @= renderMode 'dom' @= tolerateWhitespaces true}
{/template}
Модификаторы вида async и генератор наследуются дочерними шаблонами без возможности переопределения этого поведения.
- namespace demo
- async template base()
- var data = await db.getData()
/// Тоже самое, что и
/// async template sub() extends @base
- template sub() extends @base
{namespace demo}
{async template base()}
{var data await db.getData() /}
{/template}
/// Тоже самое, что и
/// async template sub() extends @base
{template sub() extends @base}
{/template}
Декораторы шаблонов также наследуются, а дочерний шаблон может добавить новые, которые применятся после родительских.
- namespace demo
- myDecorator1
- myDecorator2
- template base()
- myDecorator3
- template sub() extends @base
{namespace demo}
{myDecorator1}
{myDecorator2}
{template base()}
{/template}
{myDecorator3}
{template sub() extends @base}
{/template}
Вызываемы блоки — это подвид блоков, которые могут принимать входные параметры и неоднократно применяться в шаблоне. Наследование содержимого таких блоков осуществляется также, как и у простых блоков, а входные параметры — по общей схеме.
Дополнительно следует отметить, что если блок является немедленно вызываемым, то это поведение также наследуется.
- namespace demo
- template base()
- block hello(name) => 'friend'
Hello {name}!
- template sub() extends @base
- block hello(name) => 'world'
- super
{namespace demo}
{template base()}
{block hello(name) => 'friend'}
Hello {name}!
{/}
{/template}
{template sub() extends @base}
{block hello(name) => 'world'}
{super}
{/}
{/template}
Результат работы шаблона sub:
Hello world!
Константы — это специальный вид переменных, которые могут явно переопределяться в дочернем шаблоне и имеют глобальную область видимости в рамках своего шаблона. В шаблоне не может быть двух констант с одинаковым именем, а также переменных, имя которых совпадает с именем константы (за исключением входных параметров функций).
- namespace demo
- template base()
- title = 'Заголовок'
< title
{title}
- template sub() extends base
- title = 'Новый заголовок'
{namespace demo}
{template base()}
{title = 'Заголовок'}
{< title}
{title}
{/}
{/template}
{template sub() extends base}
{title = 'Новый заголовок'}
{/template}
Допускается вводить новые константы в дочернем шаблоне — они вставятся сразу после родительских.
- namespace demo
- template base()
- title = 'Заголовок'
< title
{title}
- template sub() extends @base
- title = 'Новый заголовок'
- bar = 'foo'
{namespace demo}
{template base()}
{title = 'Заголовок'}
{< title}
{title}
{/}
{/template}
{template sub() extends @base}
{title = 'Новый заголовок'}
{bar = 'foo'}
{/template}
Если константа была вызываемой, то это поведение также наследуется.
- namespace demo
- template base()
< title
- title = 'Заголовок' ?
- template sub() extends @base
- title = 'Новый заголовок'
{namespace demo}
{template base()}
{< title}
{title = 'Заголовок' ?}
{/}
{/template}
{template sub() extends @base}
{title = 'Новый заголовок'}
{/template}