Мова опису шаблонів Snakeskin

Snakeskin
This is Frank, a snake-cowboy who loves templates.


Привіт! Хочу розповісти про свою розробку   мовою програмування текстових шаблонів «Snakeskin». Проектом більше трьох років, всіма дитячими хворобами, я гадаю, він благополучно перехворів (і вилікувався), тому хочу поділитися результатом.
Демка
Основний репозиторій
Документація
Плагіни для Gulp, Grunt, Webpack і інше
Gitter   тут можна задати будь-яке питання
Трохи історії
Коли я працював у Яндексі (чотири роки тому), одній з основних тем для палких дискусій на кава-пойнтах у нас з колегами були шаблонизаторы: ми обговорювали переваги і недоліки існуючих рішень, деякі навіть розробляли свої власні.
 відділі основним був TemplateToolkit2   популярний в зокрема у Perl-розробників шаблонизатор, а на клієнті використовувався найпростіший MicroTemplate (by Джон Резиг). Ще в  час активно форсились XSLT-подібні двигуни, але  ряду причин (обговорення яких лежить за рамками цієї статті) нам вони не підійшли. Час від ми експериментували і з іншими: Handlebars, Dust, Closure Templates, плюс свої велосипеди, звичайно ж… Все це призвело до наявності цілого зоопарку шаблонизаторов в проекті.
Моїм фаворитом був Google Closure Templates: він був близький мені, як програмісту, т. к. шаблон позиціонувався як функція, яка повертає рядок, плюс вельми непогані тим часам фічі; але дуже засмучувала необхідність правити код на Java, щоб додати який-небудь банальний фільтр, так і швидкість трансляції була не дуже (це реально відчувалося).
 я захотів зробити свій власний Closure Templates з блекджеком і повіями: природно, щоб був написаний на JS і, як наслідок, відкритий до модифікаціям без необхідності знати Java. Плюс, мені сподобалася модель наслідування шаблонів, заснована на статичних блоках, яку я підглянув у Django Templates (звідси і назву   відсилання до Python) — вона-то і лягла в основу існуючої системи спадкування.
Прототип я накидав дня за три: це був страшний хардкод на регулярках в сім сотень рядків коду. З результатом я трохи погрався, поділився з колегами, одержав який-ніякий, але фідбек, і вирішив рухатися далі. Порефакторил це справа, поправив баги, додав нові можливостей. Після тижня розробки я зарелизил версію 2   суті, той ж хардкод на регулярках, але стабільніше і фичастей. Його вже можна було використовувати.
Попрацювавши деякий час з результатом і випустивши з десяток оновлень, я, потираючи руки, сів за комп'ютер думкою «it's time to make things right», і десь через місяць випустив 3-ю версію: викинув хардкод, переписав код на ES6 (в  не було нормальних трансляторів, тому я ще  транслятор свій власний запив (знову з моторошним хардкодом на регулярках — так-так, я люблю регулярки)), додав побудова дерева при парсингу і багато нових фіч.
Версія вийшла стабільною, потужної і суті, являла собою Closure Templates на стероїдах. Я був задоволений результатом і став використовувати Snakeskin в своїх особистих проектах, час від часу випускаючи нові оновлення і патчі.
Трохи пізніше я познайомився з HAML і Jade, мені сподобався підхід до синтаксису, і було вирішено додати в Snakeskin щось подібне (результатом цього рішення став Jade-like синтаксис). Через кілька місяців активної розробки я випустила четверту версію, що стала воістину віхою в історії мови і визначила його подальший розвиток. П'ята і шоста були не більш ніж модифікацією четвертої версії, але з ломающими змінами, які були необхідні, а в якості патерну версионирования для Snakeskin мною був обраний SemVer   довелося апать мажорну версію.
SS6 я використовував досить довго і в самих різних проектах, також його стали використовувати мої знайомі і колеги   зрештою, після деякого часу, накопичився список претензій   не дуже довгий, але  ж: фіч було багато, з'являлися в мовою вони досить хаотично, і стали видні «конфлікти» між директивами. Причиною цього була відсутність будь-якої початкової специфікації мови   розробка йшла міру появи «хотілок».
Я вирішив, що далі так жити не можна   потрібно все стандартизувати і видалити сміття. Розробка затягнулася на півтора року (з яких, правда, активна була максимум півроку   позначався брак вільного часу), але, в підсумку, вийшов самий стабільний і продуманий на даний момент реліз Snakeskin: версія 7; і я щиро їм пишаюся.
Перший погляд
Найбільш підходящим для Snakeskin мені здається визначення, що він   просто «цукор» над JS, як CoffeeScript або TypeScript, але має досить вузьку спеціалізацію: написання шаблонів. Звичайно, цілком можна написати на SS хоч всі програму цілком, але це буде, хех, не дуже зручно. SS призначений для використання разом з основною мовою   переважно JS:
select.ss
namespace select

- template main(options)
< select
- forEach options => el
< option value = ${el.value}
{el.label}

select.js
import { select } from 'select.ss';

class Select {
constructor(options) {
this.template = select.main(options);
}
}

const newSelect = new Select([{value: 1, label: 'Раз'}, {value: 2, label: 'Два'}])

Тут у основний файл на JS підключається модуль файл на Snakeskin (таку безшовну інтеграцію дає, наприклад, плагін для WebPack). З нього імпортуємо namespace
select
, і оголошуємо клас
Select
. При створенні инстанса
Select
, ми виконуємо функцію
main
(в яку транслювали шаблон
main
), і присвоюємо властивості
template
результат її роботи  
newSelect
 буде таким:
<select>
<option value="1">
Раз
</option>
<option value="2">
Два
</option>
</select>

Як бачите, SS транслюється в JS (якщо конкретно, то в ES5), який потім дуже просто використовувати в основному коді.
Якщо говорити про те, навіщо я почав робити Snakeskin   основною мотивацією було бажання мати мову шаблонів з потужними можливостями повторного використання коду, який можна використовувати на сервері і на клієнті одночасно без необхідності зміни коду шаблону. Потім, звичайно, почали з'являтися нові вимоги до мови та ідеї в стилі «а не додати мені ось таку фічу»  — все це, творчо і логічно осмислена, і зробило Snakeskin таким, яким ви його бачите зараз.
Одним з «вимог часу», наприклад, стала необхідність безшовної інтеграції з фреймворками і бібліотеками, які мають власну мову шаблонів (зразок Angular або React   ну а я волію Vue   і тепер Snakeskin це чудово вдається.
Приклад використання SS для створення шаблонів Angular:
namespace myApp
- template main()
< label
Name:
< input type = text | ng-model = yourName | placeholder = Enter a name here
< hr
< h1
Hello {{yourName}}!

Результат роботи
main

<label>
Name:
</label>
<input type="text" ng-model="yourName" placeholder="Enter a name here">
<hr>
<h1>
Hello {{yourName}}!
</h1>

Snakeskin значно скорочує кількість коду, дозволяє повторно використовувати елементи верстки (через успадкування, композицію, домішки і т. д.), а Angular здійснює data binding. З технічної точки зору SS генерує шаблон, який потім використовує Angular.
Де
  • Серверна шаблонизация   тут все просто: підключаємо SS як модуль, компілюємо файл   і node. js працює з його шаблонами як з функції:
'use strict';

const http = require('http');
const ss = require('snakeskin');

// Компілюємо файл шаблонів
// Метод поверне об'єкт з шаблонами-функціями
const tpls = ss.compileFile('./myTpls.ss');

http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/html'});

// Викликаємо шаблон foo і передаємо параметри
res.write(tpls.foo('bar', 'bla'));
res.end();
}).listen(8888);

Зрозуміло, на практиці це буде серверний фреймворк типу Express або Koa, але  має значення. Також, шаблони можна (і бажано) попередньо транслювати з допомогою плагіна для Gulp або Grunt підключати отримані файли, ну або, як вище   використовувати WebPack.
  • Генерація статичних сайтів: плагінів є опція викликати скомпільований шаблон момент трансляції і повертати результат його роботи. Плагін сам обчислить головний шаблон, або його можна вказати явно.
  • Використання транслированных в JS шаблонів на клієнт: «скомпільовані» модулі можна підключати через зовнішній тег
    <script>
    , або як модуль (з допомогою Webpack, Browserify, RequireJS або будь-якої іншої системи управліннями модулями).
Короткий огляд мови
Тут я пробегусь основним концепціям, а  вас залишаться питання   ласкаво просимо до документацію в Gitter.
Основне

Шаблони

Як вже неодноразово згадувалося, шаблон Snakeskin після трансляції стає функцією JavaScript:
namespace myApp
- template main()
Hello world!

після трансляції перетвориться у щось на кшталт:
if (exports.myApp === 'undefined') {
var myApp = exports.myApp = {};
}

exports.myApp.main = function main() {
return 'Hello world!';
}

Звичайно, це спрощений код, але в загалом це виглядає приблизно так.

Синтаксис

SS підтримує 2 різні види синтаксису:
  • Classic: директиви укладені в фігурні дужки; блочні (які можуть містити усередині себе інший код на SS) повинні бути закриті:
{namespace myApp}
{template main(name = 'world')}
Hello {name}!
{/template}

Цей режим зручно використовувати для генерації тексту з керуючими пробілами, наприклад, коду на Python Markdown.
Примітка: для генерації тексту, де часто використовуються символи фігурних дужок, в SS є спеціальний механізм.
  • Jade-like: заснований на керуючих прогалини і схожий на Jade (звідси і назва). Приклад вище з його використанням буде виглядати так:
namespace myApp
- template main(name = 'world')
Hello {name}!

Головні плюси цього синтаксису   стислість і наочність. Ідеально підходить для генерації XML-подібних структур.
Також SS підтримує змішаний синтаксис:
namespace myApp

{template hello(name = 'world')}
Hello {name}!
{/template}

- template main(name)
+= myApp.hello(name)

Детальніше про синтаксис і його види.
Інструменти code-reuse

Спадкування

 SS кожен шаблон є класом, т. е. у нього є методи і властивості, і він може успадковуватися від іншого шаблону. Дочірній шаблон може перекрити успадковані батьківські методи і властивості і додавати нові.
Приклад наслідування шаблонів.
namespace myApp

/// Метод sayHello шаблону base
/// В SS метод можна оголошувати як усередині шаблону,
/// так і поза його -- із зазначенням імені шаблону,
/// якому метод належатиме
- block base->sayHello(name = 'world')
Hello {name}!

- template base()
- doctype

< html
< head

/// Статичний блок head
/// Щоб зробити такий блок методом,
/// досить просто додати круглі дужки після імені
- block head
< title
/// Властивість шаблону `title`, яке відразу виводиться
- title = 'Головна сторінка' ?

< body
- block body
/// Виклик методу sayHello
+= self.sayHello()

/// Доопределяем батьківський метод
- block child->sayHello()
/// Викликаємо метод sayHello батьків
- super
Hello people!

/// Додаємо новий метод
- block child->go()
Let's go!

/// Шаблон child успадковується від base
- template child() extends myApp.base

/// Перевизначаємо властивість
- title = 'Дочірня сторінка'

/// Доопределяем статичний блок
- block body
- super
+= self.go()

Результат виконання
child
:
<!DOCTYPE html>
<html>
<head>
<title>Дочірня сторінка</title>
</head>

<body>
Hello world! Hello people! Let's go!
</body>
</html>

При спадкуванні шаблону також успадковуються вхідні параметри, декоратори шаблону, різні модифікатори   ось тут можна почитати докладніше.

Композиція

оскільки всі шаблони в Snakeskin це функції, то, природно, будь-який шаблон може викликати будь-який інший шаблон: для цього служить директива
call
.
namespace myApp

- template hello(name = 'world')
Hello {name}!

- template main(name)
- call myApp.hello(name)

/// Або коротка форма
+= myApp.hello(name)

Шаблон як значення

 Snakeskin можна присвоїти шаблон змінної або властивості об'єкта, передати його як аргумент у функцію, і так далі.
namespace myApp

- template wrap(content)
< .wrapper
{content}

- template main(name)
+= myApp.wrap()
< .hello
Hello world!

Результат виконання
main

<div class="wrapper">
<div class="привіт">
Hello world!
</div>
</div>

Модулі

Кожен файл, написаний на Snakeskin, являє собою модуль: глобальні змінні інкапсулюються в та всі шаблони   експортуються. Модулі можуть підключати інші модулі з допомогою директиви
include
.
Таким чином, можна легко розділяти код на логічні частини, створювати спільні бібліотеки (і навіть, можливо, фреймворки), і взагалі невідступно слідувати правилу «розділяй і володарюй».
math.ss
namespace math
- template sum(a, b)
{a + b}

app.ss
namespace myApp
- include './math'

- template main()
1 + 2 =
+= math.sum(1, 2)

Результат виклику myApp.main
1 + 2 = 3

Приємні плюшки
  • Багатий набір вбудованих директив
     Snakeskin є: директиви, семантично еквівалентні операторам в JS, такі як
    if
    ,
    for
    ,
    var
    return
    , etc; директиви, специфічні для мови шаблонів і спрощують розмітку XML-подібних структур:
    tag
    ,
    attr
    doctype
    ,
    comment
    та інші; директиви для асинхронної генерації шаблону:
    await
    ,
    yield
    parallel
    ,
    waterfall
    ; і безліч інших.
    Господині на замітку: Snakeskin — це все-таки не JavaScript, тому деякі директиви в нюанси можуть працювати не так, як працюють аналогічні оператори JS; наприклад, у змінних, оголошених через
    var
     — блокова область видимості (так працює
    let
     ES2015). У директиві
    with
    взагалі усунені архітектурні недоліки однойменного оператора з JS, що робить її в рамках SS цілком собі «good practice», і просто спрощує і прискорює написання коду.
  • Механізм фільтрів
    Filters_Everywhere.jpg
    Фільтри присутні в тому чи іншому вигляді в більшості шаблонних движків, але в SS вони   частина ядра мови, внаслідок чого використовувати їх можна буквально скрізь: при створенні змінних, в циклах, при декларації аргументів блоків і шаблонів, в директивах... загалом, взагалі скрізь.
namespace myApp
- template main((str|trim), name = ('World'|lower))
- var a = {foo: 'bar'} |json

 SS з коробки є багато корисних вбудованих фільтрів, а якщо їх не вистачить, то додати свій   элементарно.
  • Двонаправлена модульна інтеграція з JS
     програму на JS можна імпортувати шаблони SS, а Snakeskin може імпортувати модулі JavaScript (з допомогою директиви
    import
    ), підтримуючи всі основні види модулів: umd, amd, commonjs, native global.
namespace myApp
- import { readdirSync } from 'fs'

/// Виводить вміст директорії ./foo
- template main((str|trim), name = ('World'|lower))
- forEach readdirSync('./foo') => dirname
{dirname}

namespace myComponent
- template render()
< .hello
{{ this.name }}

import React from 'react';
import { myComponent } from './myComponent.ss';

const Foo = React.createClass({
render: myComponent.render
});

Для такої безшовної інтеграції, коли шаблон повертає елемент, створений з допомогою React, використовуйте Webpack-плагін c включеним прапором
jsx
.
namespace myApp
- template main()
< .hello

/// hello__wrap
< .&__wrap

/// hello__cont
< .&__cont

  • Розумна інтерполяція
    Багато директиви Snakeskin підтримують механізм інтерполяції, т. е. прокидывание динамічних значень шаблону в директиви, наприклад:
namespace myApp
- template main(area)
< ${area ? 'textarea' : 'input'}.b-${area ? 'textarea' : 'input'}
Бла бла бла

 залежно від значення
area
результат буде виглядати або так (при
area == true
):
<textarea class="b-textarea">
Бла бла бла
</textarea>

або так (при
area == false
):
<input class="b-input" value="Бла бла бла">

  • Декоратори шаблонів
    Завдяки механізму декораторів, в Snakeskin легко інтегрувати додаткові модулі   наприклад, типограф:
namespace demo
- import Typograf from 'typograf'

/// Функцію-декоратор можна написати на JS або на SS
- template typograf(params)
- return
() => target
- return
() =>
- return new Typograf(params).execute(target.apply(this, arguments))

/// Результат шаблону index завжди буде оброблений друкарем
- @typograf({lang: 'uk'})
- template index()
Спорт - це правильно!

  • Асинхронні шаблони
    SS дозволяє створювати шаблони-генератори і async-шаблони, плюс містить ряд директив для зручного використання популярної бібліотеки async.
namespace myApp

- async template main(db)
- forEach await db.getData() => el
{el}

- template *foo(data)
- for var i = 0; i < data.length; i++
{data.value}

- if i % 1e3 === 0
- yield

Також зазирни в розділ «Директиви для асинхронної роботи».
  • Настроюється рендеринг
     коробки Snakeskin підтримує чотири режими візуалізації: рядок (за замовчуванням), в Buffer, в DocumentFragment і в JSX; також є можливість додати свій рендерер   наприклад, щоб згенерувати кастомный Virtual DOM.
  • Інформативні повідомлення про помилки
     транслятор Snakeskin вбудований потужний відладчик коду, який допомагає знаходити більшість синтаксичних і логічних помилок при трансляції шаблонів.
  • Підтримка всіма основними системами збірок
    Gulp, Grunt, WebPack.
  • Гарна кодова база
    Snakeskin повністю написаний на ES2015, містить велику кількість тестів і проходить максимально сувору перевірку Google Closure Компілятор режимі
    ADVANCED
    . Код добре документований в згідно з стандартом JSDoc від Google.
  • Докладна і зрозуміла документація
    Яка, до речі, написана на Snakeskin.
Висновок
Щиро сподіваюся, що Snakeskin вас зацікавив, ви спробуєте його і будете з задоволенням користуватися.
Висловлюю щиру вдячність trikadin за допомога з написанням і редактурою статті. До речі, цей хлопець працює фронтэндером в «Едадиле», і зараз вони проводять у себе впровадження Snakeskin як основного мови шаблонів для Web. Каже, що він щасливий і не розуміє, як жив без SS раніше:)
Також хочу подякувати колективу форуму javascript.ru за ідеї по розвитку мови і підтримку.
 знайдені баги пишіть  Issues GitHub-e проекту, а з'явилися питання задавайте тут в коментарях, або  Gitter'е   я з задоволенням відповім і поясню.
Удачі!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.