• Введение

Что такое Snakeskin

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 и поддерживает работу как в браузере, так и в других окружениях:

  • node.js (0.12+);
  • node-webkit;
  • electron;
  • web-worker;
  • дополнения к приложениям и т. д.

Запуск в браузере

  • runtime — микробиблиотека для запуска скомпилированных шаблонов;
  • compiler — для запуска компилятора необходимо предварительно установить ряд зависимостей и полифилов (если нужна поддержка старых браузеров).

Bower

bower install --save snakeskin

Файлы библиотеки лежат по адресу:

bower_components/
	snakeskin/
		dist/
			snakeskin.min.js
			snakeskin.live.min.js

NPM

npm install --save snakeskin

Плагины

std.ss

std.ss — это библиотека написанная на Snakeskin, которая предоставляет ряд дополнительных функций для разработки.

GitHub

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

Основные use-case Snakeskin — это:

  1. Серверная шаблонизация;
  2. Предварительная трансляция шаблонов в JS и использование их на клиенте;
  3. Генерация шаблонов для различных фреймворков, например, Angular, Vue, React и т. д.;
  4. Статическая генерация страниц на этапе сборки проекта.

Также есть возможность «живой» компиляции в браузере, хотя данная фича не рекомендуется для prod.

Песочница

Простейшим способом изучения возможностей Snakeskin является использование онлайн-песочницы на Codepen.

Использование Snakeskin как серверного шаблонизатора вместе с Node.js

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()
);

Подключение уже скомпилированного шаблона в Node.js

Скомпилированные файлы Snakeskin подключаются как простые Node.js модули.

index.js

var tpls = require('./templates.ss.js');

Использование вместе с Vue и Webpack

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, чтобы обеспечить поддержку в старых браузерах.

Статическая генерация страниц вместе с Gulp

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.

Статическая генерация с 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}

jsDoc

Snakeskin поддерживает jsDoc комментарии, которые не вырезаются из конечного JavaScript кода.

/**
 * jsDoc комментарий
 * @param {string} name
 */
{template index(name)}
 /*
  * Обычный комментарий
  */
{/template}

Экранирование

Символ \ может использоваться для экранирования директив, комментариев и прочих сущностей Snakeskin.

{template example()}
	\{var val = 1} /// Выведется как простой текст
	\/// Выведется как простой текст
	\\1 /// \1
{/template}
  • Основы

Jade-like синтаксис

Помимо основного синтаксиса 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 биндинг

Оператор @ осуществляет привязку параметра к директиве 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}