Dokumentacja

Składnia szablonów

Wszystko, co rozumie parser, mieści się na tej stronie. Jest pięć konstrukcji — interpolacja, bloki, filtry, partiale i komentarze — i żadnego sposobu na osadzenie dowolnego JavaScriptu.

Interpolacja

Podwójne nawiasy klamrowe wstawiają wartość. Wynik jest escapowany jako HTML, a reguły zależą od tego, gdzie stoi interpolacja.

<p>{{user.name}}</p>                  // text context
<a href="{{link}}">…</a>            // URL context
<div style="width:{{w}}px">…</div>   // style context

W kontekście URL wartość, której schemat nie jest http, https, mailto ani ścieżką względną, jest zastępowana przez about:blank. Celowo nie da się tego skonfigurować.

Wyłączanie escapowania

Potrójne nawiasy wstawiają wartość dosłownie:

<div class="body">{{{post.renderedHtml}}}</div>
Niebezpieczne z definicji

Potrójne nawiasy to jedyne miejsce, w którym Verso może wytworzyć wstrzyknięcie. Kompiluj z { allowRaw: false }, by uczynić je błędem kompilacji w całym projekcie — przydatne, gdy szablony piszą osoby, które nie robią przeglądów bezpieczeństwa.

Warunki

Bloki otwiera #, a zamyka /. Warunki przyjmują ścieżkę, porównanie albo ścieżkę przepuszczoną przez filtry — nic więcej.

{{#if user}}
  Welcome back.
{{#elseif invite.valid}}
  You were invited by {{invite.from}}.
{{#else}}
  <a href="/sign-in/">Sign in</a>
{{/if}}

Prawdziwość działa jak w JavaScripcie, z jedną zmianą: pusta tablica jest fałszywa. Na tę decyzję złożyło się sześć lat błędów w szablonach.

Operatory porównania to ==, !=, <, <=, >, >= i in. Równość jest ścisła; nie ma luźnego wariantu.

{{#if order.status == 'shipped'}}…{{/if}}
{{#if role in allowedRoles}}…{{/if}}
{{#unless flags.beta}}…{{/unless}}

Pętle

#each iteruje po tablicach, obiektach Map, Set i zwykłych obiektach. Opcjonalna klauzula as nazywa element i, jako drugie, indeks lub klucz.

{{#each releases as release, i}}
  <li class="{{#if i == 0}}latest{{/if}}">
    {{release.version}} — {{release.date | date: 'MMM yyyy'}}
  </li>
{{#else}}
  <li>No releases yet.</li>
{{/each}}

Gałąź #else pętli wykonuje się, gdy kolekcja jest pusta lub jej brak. W treści dostępne są cztery zmienne:

ZmiennaZnaczenie
@indexPozycja liczona od zera
@firsttrue w pierwszej iteracji
@lasttrue w ostatniej iteracji
@lengthŁączna liczba elementów
Uwaga

Zagnieżdżone pętle przesłaniają te zmienne. Aby dostać się do zewnętrznej, nazwij ją w klauzuli as — Verso celowo nie ma składni ../ dla ścieżek nadrzędnych.

Filtry

Kreska pionowa stosuje filtr. Filtry łączą się od lewej do prawej i przyjmują argumenty pozycyjne po dwukropku.

{{title | upper}}
{{post.body | strip | truncate: 280, '…'}}
{{price | number: 2 | prepend: '€'}}

Wbudowanych filtrów jest osiemnaście:

FiltrArgumentyDziałanie
upper / lower—Zmiana wielkości liter, z uwzględnieniem języka
capitalize—Pierwsza litera wielka, reszta bez zmian
trim / strip—Przytnij białe znaki / usuń wszystkie znaczniki HTML
truncatedługość, wielokropekPrzycina na granicy słowa
numbermiejsca po przecinkuStała liczba miejsc po przecinku z grupowaniem
datewzorzec, strefa czasowaFormatuje Date lub łańcuch ISO
joinseparatorŁączy tablicę
sortkluczStabilne sortowanie, opcjonalnie po właściwości
reverse / unique—Pomocnicze dla tablic
first / lastliczbaWycinek z dowolnego końca
defaultwartośćZastępuje pusty, null lub undefined
jsonwcięcieSerializuje, bezpiecznie wewnątrz <script>
prepend / appendtekstŁączenie

Własne filtry

Zarejestruj własne przy kompilacji. Filtr to czysta funkcja; najpierw dostaje wartość, potem argumenty.

const page = await compileFile('views/page.vs', {
  filters: {
    money: (v, currency = 'EUR') =>
      new Intl.NumberFormat('en', { style: 'currency', currency }).format(v)
  }
});

Wynik filtrów jest escapowany jak każda inna wartość. Zwrócenie znaczników z filtra wymaga opakowania ich w raw(), eksportowane właśnie w tym celu i łatwe do znalezienia podczas recenzji.

Partiale i sloty

> dołącza inny szablon. Ścieżki są rozwiązywane względem pliku dołączającego.

{{> './partials/card.vs'}}
{{> './partials/card.vs' with item}}
{{> './partials/card.vs' with { title: post.name, tone: 'muted' }}

Bez with partial widzi bieżący obiekt danych. Z tą klauzulą widzi tylko to, co mu przekażesz — dzięki temu partiale można testować w izolacji.

Układy

Układy to partiale z nazwanymi slotami:

layouts/base.vs
<main>{{#slot}}{{/slot}}</main>
<aside>{{#slot 'sidebar'}}Nothing here.{{/slot}}</aside>
views/article.vs
{{#extends '../layouts/base.vs'}}
  {{#fill}}<article>{{{post.html}}}</article>{{/fill}}
  {{#fill 'sidebar'}}{{> '../partials/toc.vs'}}{{/fill}}
{{/extends}}

Treść wewnątrz #slot jest zastępcza, gdy żaden #fill nie pasuje. Szablon może rozszerzać dokładnie jeden układ, a układy mogą rozszerzać inne układy.

Odroczone bloki

W trybie strumieniowym #defer oznacza blok, którego dane nie są jeszcze gotowe. Otaczająca strona jest wysyłana od razu, a blok przychodzi, gdy jego obietnica się rozstrzygnie.

<section>
  {{#defer recommendations as list}}
    {{#each list as item}}<li>{{item.title}}</li>{{/each}}
  {{#pending}}
    <p class="skeleton">Loading recommendations…</p>
  {{#failed as err}}
    <p>Could not load recommendations.</p>
  {{/defer}}
</section>
Renderowanie do łańcucha

Poza renderToStream() #defer po prostu czeka na obietnicę w miejscu. Gałąź #pending nigdy nie jest wypisywana, a odrzucona obietnica renderuje #failed zamiast rzucać wyjątek.

Białe znaki i komentarze

Myślnik wewnątrz nawiasów przycina sąsiednie białe znaki, co ma znaczenie przy generowaniu czegokolwiek poza HTML:

{{#each rows as r -}}
  {{r.a}},{{r.b}}
{{/each}}

Komentarze nigdy nie trafiają do wyniku:

{{!-- reviewed 2026-02-14, keep in sync with the API schema --}}