Синтаксис Jet
Jet — движок шаблонов для Go. Синтаксис близок к text/template из Go, плюс то, что взято из Jinja2 и Twig: наследование через extends и блоки, фильтры через |, тернарный оператор условие ? a : b.
Если вы знаете шаблоны Go, держите в голове главные отличия:
- переменные без
$:x := note.Title(), а не$x := .Title; - методы вызываются со скобками:
note.Title(); - операторы как в C:
==,&&,!,+, а не функцииeq,and,not; ifс объявлением принимает любое условие:if x := f(); x != "";rangeс одной переменной даёт индекс, а не элемент;- в пайпе
a | f: bзначениеaстановится первым аргументом, а в Go — последним; - повторяемый кусок —
blockс именованными параметрами иyield, а неdefineиtemplate; - экранирование одно и то же везде, без учёта контекста, как в
html/template.
Если вы знаете Jinja2 или Twig: extends, блоки, import, include и фильтры работают похоже, но все теги пишутся в {{ }}, а цикл — это range, а не for x in list. Полное сравнение — в Справочнике функций Jet.
Справочник синтаксиса от автора движка
Все встроенные функции, фильтры и то, что добавляет trip2g, — в Справочнике функций Jet. Как собирать шаблон из компонентов — в Компонентах шаблонов.
Вывод переменных
Двойные фигурные скобки выводят значение:
{{ note.Title() }}
{{ user.Firstname }}
Методы вызываются со скобками: {{ note.Title }} без () выведет адрес функции, а не заголовок.
Вывод экранируется: {{ value }} выводит < и & текстом. Методы, которые возвращают готовый HTML (note.HTMLString(), ContentHTML и другие), выводятся как есть. Подробнее — в разделе #Экранирование.
Доступ к полям структур, методам, элементам массивов и карт:
{{ user.Fullname() }}
{{ items[0] }}
{{ config["theme"] }}
Объявление переменных
{{ item := items[0] }}
{{ name := "Иван" }}
Комментарии
{* это комментарий, не попадёт в HTML *}
Условия
if / else / else if
{{ if note.Title() }}
<h1>{{ note.Title() }}</h1>
{{ else }}
<h1>Без заголовка</h1>
{{ end }}
{{ if len(items) > 0 }}
Есть элементы
{{ else if len(items) == 0 }}
Пусто
{{ end }}
Объявление в условии
{{ if value, ok := myMap["key"]; ok }}
{{ value }}
{{ end }}
{{ if about := nvs.ByPermalink("/about"); about }}
{{ about.Title() }}
{{ else }}
Страницы нет
{{ end }}
Переменная видна только внутри if, его else if и else. Подробнее — в присваивании в if.
Тернарный оператор
{{ note.Title() ? note.Title() : "Заголовок не задан" }}
Циклы
range — перебор
Важно: при одной переменной
rangeотдаёт индекс (0, 1, 2…), а не значение.
Чтобы получить значение, всегда указывайте две переменные.
{* item = 0, 1, 2 — это индексы, не значения! *}
{{ range item := items }}
{{ item }}
{{ end }}
Чтобы получить значения — используйте две переменные:
{{ range idx, item := items }}
{{ idx }}: {{ item }}
{{ end }}
Если индекс не нужен — поставьте _ вместо первой переменной:
{{ range _, item := items }}
{{ item }}
{{ end }}
Блок else — если коллекция пуста:
{{ range i, item := items }}
{{ item }}
{{ else }}
Список пуст
{{ end }}
Срезы (slice) — начало:конец, конец не включается. Границы могут быть переменными:
{{ from := 1 }}
{{ to := len(items) - 1 }}
{{ range i, item := items[from:to] }}
{{ item }}
{{ end }}
Подробнее и с выводом — в Справочнике функций Jet.
Операторы
Арифметика
+, -, *, /, %
{{ 1 + 2 * 3 }}
{{ (10 - 2) / 4 }}
Сравнение
==, !=, <, >, <=, >=
{{ if count > 0 }}...{{ end }}
Логические
&& (и), || (или), ! (не)
{{ if isAdmin && isActive }}...{{ end }}
{{ if !isDeleted }}...{{ end }}
Конкатенация строк
{{ "Привет, " + user.Name + "!" }}
Фильтры (пайплайны)
Фильтры преобразуют значения. Передаются через |:
{{ "ТЕКСТ" | lower }}
{{ name | upper }}
Цепочка фильтров:
{{ text | lower | trimSpace }}
Встроенные фильтры
| Фильтр | Описание |
|---|---|
lower |
В нижний регистр |
upper |
В верхний регистр |
trimSpace |
Убрать пробелы по краям |
split |
Разбить строку |
replace |
Заменить подстроку |
repeat |
Повторить строку |
hasPrefix |
Начинается с... |
hasSuffix |
Заканчивается на... |
Экранирование
| Фильтр | Описание |
|---|---|
html |
Функция: экранирует HTML один раз и возвращает значение. Для текста не нужна, вывод и так экранируется. Нужна, чтобы показать HTML-код текстом: {{ html(note.HTMLString()) }} |
safeHtml |
То же экранирование, но работает только последним фильтром в теге вывода |
url |
Экранировать одно значение параметра URL |
unsafe / raw |
Без экранирования; только последним фильтром в теге вывода |
json / writeJson |
Преобразовать в JSON |
safeJs |
Безопасный вывод в JS-строку |
Чем html отличается от safeHtml и когда нужны url, raw, unsafe — с примерами в Справочнике функций Jet.
Функции
isset — проверка на существование
{{ isset(author) ? author : "Аноним" }}
Неопределённая переменная внутри isset — не ошибка, а false.
Работает и для проверки ключей в карте:
{{ if isset(config["theme"]) }}...{{ end }}
len — длина
{{ len(items) }}
{{ if len(text) > 100 }}...{{ end }}
Блоки
Блоки — фрагменты, которые вызываете в разных местах.
Определение блока
{{ block card(title="", text="") }}
<div class="card">
<h3>{{ title }}</h3>
<p>{{ text }}</p>
</div>
{{ end }}
Задавайте значение по умолчанию каждому параметру. Блок, объявленный в самой странице, выводится ещё и там, где объявлен, — без аргументов, и параметр без значения по умолчанию падает с missing name for block parameter. Слово content зарезервировано: так нельзя назвать ни параметр, ни блок.
Параметры по умолчанию:
{{ block button(text="", type="primary") }}
<button class="btn-{{ type }}">{{ text }}</button>
{{ end }}
Вызов блока
Важно: параметры передаются по имени, не по позиции. Позиционные аргументы игнорируются, параметры остаются со значениями по умолчанию.
{* Правильно — именованные параметры *}
{{ yield card(title="Заголовок", text="Текст") }}
{* Неправильно — позиционные параметры *}
{{ yield card("Заголовок", "Текст") }} {* title и text останутся пустыми *}
Порядок параметров неважен:
{{ yield card(text="Текст", title="Заголовок") }} {* работает *}
Параметры с дефолтами можно не передавать:
{{ yield button(text="Отправить") }} {* type="primary" по умолчанию *}
{{ yield button(text="Отмена", type="secondary") }}
Блок с вложенным контентом
Определение:
{{ block link(href="") }}
<a href="{{ href }}">{{ yield content }}</a>
{{ end }}
Вызов:
{{ yield link(href="https://example.com") content }}
Перейти на сайт
{{ end }}
Рекурсивные блоки
{{ block menu() }}
<ul>
{{ range i, item := . }}
<li>
{{ item.Name }}
{{ if len(item.Children) > 0 }}
{{ yield menu() item.Children }}
{{ end }}
</li>
{{ end }}
</ul>
{{ end }}
{{ yield menu() navItems }}
Композиция шаблонов
import — импорт блоков
Загружает блоки из другого файла. Странице, которая вызывает блок через yield, импорт обычно не нужен: trip2g импортирует файл сам (см. автоимпорт).
{{ import "blocks" }}
{{ yield main_layout() content }}
...
{{ end }}
include — вставка шаблона
Вставляет шаблон целиком с передачей данных:
{{ include "partials/user-card" user }}
Условная вставка:
{{ if ok := includeIfExists("sidebar"); !ok }}
<p>Боковая панель не найдена</p>
{{ end }}
extends — наследование layout
Шаблон наследует базовый layout и переопределяет его блоки:
{{ extends "base" }}
{{ block main() }}
<p>This replaces the main block of base.html</p>
{{ end }}
extends должен быть первым тегом шаблона. Что такое базовый шаблон, что где выводится и как наследование сочетается с компонентами, — в Шаблонах: наследование, с проверенным примером.
exec и return — данные из другого файла
exec("путь", данные) выполняет другой файл из _layouts/ и возвращает значение, которое этот файл отдал через return. Всё, что файл напечатал, отбрасывается — назад приходит только значение. Внутри файла данные доступны как ., переменные страницы (note, nvs, …) работают как обычно.
exec — для данных, а не для разметки. Компонент ({{ block }} + {{ yield }}) выдаёт HTML; файл для exec выдаёт список, map или число, а вызывающий шаблон рисует их по-своему. Главный случай — универсальный способ задать выборку в одном месте: какие заметки раздела считаются «избранными», какая у раздела статистика. Файл вызывают несколько шаблонов, а правило живёт в одном месте.
Избранные заметки раздела. _layouts/lib/featured.html:
{{ return nvs.ByGlob(. + "/*.md").Public().SortByMeta("order").Limit(3).All() }}
Любой шаблон получает избранное нужного раздела:
<ul class="featured">
{{ range _, post := exec("lib/featured", "blog") }}
<li><a href="{{ post.Permalink() }}">{{ post.Title() }}</a></li>
{{ end }}
</ul>
Поменять смысл «избранного» — другая сортировка, пять заметок вместо трёх, только с флагом во frontmatter — это одна правка в lib/featured.html.
Статистика раздела в виде map. _layouts/lib/section_stats.html:
{{ notes := nvs.ByGlob(. + "/*.md").Public().All() }}
{{ minutes := 0 }}
{{ range _, n := notes }}
{{ minutes = minutes + n.ReadingTime() }}
{{ end }}
{{ return map("count", len(notes), "minutes", minutes) }}
{{ stats := exec("lib/section_stats", "blog") }}
<p>{{ stats["count"] }} posts, {{ stats["minutes"] }} min of reading</p>
Что важно знать:
- Путь отсчитывается от
_layouts/и пишется без расширения.exec("lib/featured")иexec("/lib/featured")загружают_layouts/lib/featured.htmlиз любой папки.exec("lib/featured.html")файл не найдёт. Шаблонами считаются только файлы.htmlи.html.jsonвнутри_layouts/. - Несуществующий файл роняет рендер с ошибкой
template /lib/featured could not be found. Если страница должна это пережить — оберните вызов вtry(ниже). returnимеет смысл только в файле, который вызывают черезexec. В обычной странице он ничего не останавливает и ничего не выводит.- Компоненты внутри файла для
execнужно импортировать явно. Автоимпорт работает только для самой страницы; файл, до которого дошли черезexec,includeилиextends, должен сам написать{{ import "/путь/к/компоненту" }}(trip2g#388). - Каждый вызов выполняет файл заново.
execне кэшируется:lib/section_statsв цикле по двадцати разделам — это двадцать полных выборок на каждый рендер. Если значение зависит только от самой заметки, дешевле frontmatter; тяжёлые вычисления по многим заметкам лучше делать Go-хелпером.
Обработка ошибок: try / catch
{{ try }}…{{ catch err }}…{{ end }} рендерит внутреннюю часть, а если в ней что-то упало — выбрасывает её вывод и рендерит часть catch. Остальная страница рендерится как обычно. Без catch упавший try не выводит ничего.
Оборачивайте виджет, входные данные которого вы не контролируете, — тогда одна плохая заметка покажет заглушку вместо страницы с ошибкой.
Виджет графика из frontmatter. Если chart нет или он кривой, читатель увидит маленькую карточку, а не сломанную страницу:
{{ try }}
{{ chart := note.M().Get("chart") }}
<figure class="chart">
<figcaption>{{ chart["title"] }}</figcaption>
{{ range _, v := chart["values"] }}
<span class="bar" style="--v: {{ v }}"></span>
{{ end }}
</figure>
{{ catch err }}
<div class="chart chart--broken">
Chart unavailable: {{ err.Error() | html }}
</div>
{{ end }}
Необязательная связанная заметка. Поле related во frontmatter может указывать на переименованную или удалённую заметку. Если поиск или любой вызов на ней упадёт, блока просто не будет:
{{ try }}
{{ related := nvs.ByPath(note.M().GetString("related", "")) }}
<aside class="related">
See also: <a href="{{ related.Permalink() }}">{{ related.Title() }}</a>
</aside>
{{ end }}
Строка статистики из файла-помощника. Если lib/section_stats сломается, посетители ничего не увидят, а админ сайта увидит причину:
{{ try }}
{{ stats := exec("lib/section_stats", "blog") }}
<p>{{ stats["count"] }} posts, {{ stats["minutes"] }} min of reading</p>
{{ catch err }}
{{ if currentUser.IsAdmin() }}
<p class="admin-error">
lib/section_stats: {{ err.Error() | html }}
</p>
{{ end }}
{{ end }}
Что держать в голове:
catch— для заглушек, а не чтобы прятать ошибки.tryвокруг половины страницы превращает любую ошибку в тишину. Оборачивайте минимальный кусок, который может законно упасть, и показывайте ошибку админам (как выше), чтобы сломанный виджет заметили. Обращение кcurrentUserделает страницу персонализированной — она не отдаётся из анонимного кэша страниц.- Экранируйте сообщение. В
err.Error()может попасть текст из заметки или URL — пропускайте его черезhtml. Фильтр экранирует ровно один раз, включено экранирование вывода по умолчанию или нет. tryне ловит шаблон, который не загрузился. Синтаксическая ошибка илиextendsне на своём месте останавливают шаблон до рендера; это видно как предупреждение шаблона, а не какcatch.
Отладка шаблонов
debug() — тип, значение и методы объекта
Глобальная функция debug() возвращает строку с Go-типом, значением и списком методов любого выражения:
{{ debug(note.M()) }}
{* → *templateviews.Meta: &{raw:map[title:My Page extra_content:[a b]]}
methods: [Debug Get GetBool GetInt GetString GetStrings Has Raw] *}
{{ debug(note.Title()) }}
{* → string: My Page *}
Всегда добавляйте скобки при передаче методов:
debug(note.Title())— правильно,debug(note.Title)— вернёт ссылку на функцию.
Meta.Debug() — JSON frontmatter
{{ note.M().Debug() }}
{* → {"extra_content":["channels","prices"],"title":"My Page"} *}
Для полного гайда по отладке шаблонов через /_system/renderlayout — см. skills/check_templates.
Полезные ссылки
- Справочник функций Jet — все функции и конструкции, проверенные на trip2g
- Справочник синтаксиса
- Встроенные функции