You’re reading the ZotLit v2 beta docs. Still on v1? Read the v1 docs

ZotLit

Eta template syntax

Eta (JavaScript) template reference: delimiters, whitespace control, auto-filter, helpers, and the zt variable.

This page documents the Eta template language as configured in ZotLit. Eta templates require the JavaScript Templates gate to be enabled. For the default Liquid engine, see Template syntax.

All template data is accessible through a single variable, zt. The shape of zt depends on the template type: see the Data reference for the full property list.

Delimiters

DelimiterPurpose
<% %>Execute JavaScript (no output)
<%= %>Output an expression (auto-filtered)
<%~ %>Output an expression (auto-filtered)
<%/* */%>Comment (stripped from output)

<%= %> and <%~ %> are functionally identical in ZotLit. Both pass their result through the auto-filter. Use either for output; <%= %> is conventional.

<%= zt.title %>
<% if (zt.abstract) { %>
<%= zt.abstract %>
<% } %>

Use <%/* */%> for comments that do not appear in the rendered note:

<%/* Draft: revisit multi-author rendering */%>

No hash-prefix comments

Eta does not support <%# %>. That syntax causes a compile error. Use <%/* */%> for all comments.

Whitespace control

By default, ZotLit sets autoTrim to [false, false]: whitespace around tags is preserved exactly as written. Newlines after %> and indentation before <% appear in the rendered note.

Per-tag markers override this default on one side of the tag:

MarkerPositionEffect
-Opening (<%-)Trims one newline before the tag
-Closing (-%>)Trims one newline after the tag
_Opening (<%_)Trims all whitespace before the tag
_Closing (_%>)Trims all whitespace after the tag
<% for (const annotation of zt.annotations) { -%>
- <%= annotation.text %>
<% } %>

The trailing -%> consumes the newline immediately after the opening tag, so each iteration begins on the - line.

Global autoTrim setting

Two settings control default trim behavior when the JavaScript Templates gate is on:

  • Trim whitespace before tags: applies to the opening side of every tag.
  • Trim whitespace after tags: applies to the closing side of every tag.

Each accepts three values: Keep (no trim), Strip one newline, or Strip all whitespace. Per-tag markers always override the global setting for that tag.

Markdown is whitespace-sensitive

The default (keep all whitespace) avoids silently breaking Markdown formatting. Use per-tag markers for precise control.

Auto-filter (output coercion)

Every <%= %> and <%~ %> expression passes through a filter function before rendering. The filter coerces values to Markdown-safe text:

ValueRendered as
null / undefined"" (empty string)
DateISO 8601 string (toISOString())
Temporal.InstantLocal plain date, e.g. 2026-06-21
Everything elseString(value)

This means <%= zt.someFieldThatIsNull %> outputs nothing rather than the literal text "null". Creator objects and dates render through their toString() method: creators produce their full name, dates produce ISO strings.

The zt variable

All template data lives under the zt variable. The variable name applies across all template types:

<%= zt.title %>
<%= zt.creators[0].family %>
<%= zt.citationKey %>

The editor autocomplete inserts = zt. when you select the interpolation hint inside <%= %> tags.

zt replaces v1's it variable. See Migrate v1 Eta templates for the full property mapping.

include(): composing templates

include() renders another template inline. Pass the template name and an optional data argument:

<%~ include("annotation", annotation) %>
<%~ include("content", zt) %>

The data argument becomes the child template's zt variable, direct passthrough not merged with the parent's data. When omitted, the child inherits the parent's zt.

The name resolves to a registered template: "annotation" finds zotlit-annotation.eta.md. It is a name, not a file path.

The content include

When include("content", zt) is called from the note template, ZotLit automatically wraps the output with managed-region markers (%%zt-managed%% / %%/zt-managed%%). These markers never appear in any .eta.md file. They are injected at render time.

bq(): blockquote helper

Wraps its content in a Markdown blockquote. Every line is prefixed with > . The content is trimmed. Consecutive blank lines inside the block collapse into a single bare >.

<% bq(() => { %>
[!note] Page <%= zt.pageLabel %>

<%= embed(zt.imgLink) %><%= zt.text %>
<% if (zt.comment) { %>

<%= zt.comment %>
<% } %>
<% }) %>

Rendered output:

> [!note] Page 42
>
> ![[excerpt.png]]The highlighted passage
>
> My thoughts on this

bq() must appear in a <% %> execute tag. It captures all output produced inside the callback, trims the result, prefixes each line with > (empty lines become bare >), collapses consecutive bare > lines into one, and writes the result to the output.

When the captured content is empty, bq() renders a lone >.

embed(): embed helper

Turns a link helper into a Markdown embed by prefixing !. Returns "" when the link is null, undefined, or renders empty.

<%= embed(zt.imgLink) %>
<%= embed(zt.imgLink, "alt text") %>

Signature: embed(link, alias?, subpath?)

InputOutput
zt.imgLink renders "[[excerpt.png]]""![[excerpt.png]]"
zt.imgLink is null""

suffix(): filename collision guard

For the filename template only. Appends a random string when the generated filename collides with an existing note. When the name is free, it renders nothing.

<%= zt.citationKey ?? zt.DOI ?? zt.title ?? zt.key %><%= suffix() %>
  • First note named Smith2020 produces Smith2020.md.
  • A second note that would also render Smith2020 produces Smith2020_a1b2c3.md.

Signature

suffix(length?, prepend?, append?)
ParameterDefaultConstraints
length6Integer, 1–64
prepend"_"Must not contain : or %
append""Must not contain : or %

The affixes appear only on collision. On a free name, the entire expression (affixes included) renders nothing.

<%= zt.title %><%= suffix(10) %>
<%= zt.title %><%= suffix(6, "(", ")") %>
<%= zt.citationKey %><%= suffix(4, "-") %>

The random string uses an alphanumeric alphabet. On collision, ZotLit retries up to 5 times with fresh random strings.

Links on the zt context are functions, not precomputed strings. Call them to render. Each accepts optional overrides:

linkHelper(alias?, subpath?)
  • alias: display text for the link. Omit to use the default (filename or note title).
  • subpath: a #-fragment appended to the target, e.g. "#heading" or "#page=3".
HelperAvailable onDefault subpathReturns
zt.noteLink()item contextnoneMarkdown link to the item's literature note, or null
note.noteLink()each zt.notes[] entrynoneLink to the child note
a.fileLink()each zt.attachments[] entrynoneAttachment file link
zt.fileLink()annotation context#page=NAttachment link deep-linked to the annotation's page
zt.imgLink()annotation contextnoneExcerpt-image link, or null
<%= zt.attachments[0].fileLink() %>
<%= zt.attachments[0].fileLink("Open the PDF") %>
<%= zt.noteLink("See notes", "#Summary") %>

zt.noteLink() returns null when the literature note path is unresolvable (collision, recursive resolution, or template error). zt.imgLink is null for annotation types without a cached image (everything except image and ink).

Template file naming

Template files sit directly inside the configured template folder. The naming pattern:

zotlit-<name>.<language>.md
  • <name>: alphanumeric characters or hyphens ([A-Za-z0-9-]+).
  • <language>: liquid or eta.

The six canonical names: filename, note, annotation, content, cite, cite2.

Each canonical name corresponds to a template file: the filename template is stored as zotlit-filename.eta.md, following the same pattern as the other five types.

Liquid-wins shadowing

When both a .liquid.md and .eta.md file exist for the same name, the Liquid file takes precedence. The Eta file is ignored.

No async

ZotLit renders templates synchronously. async/await is not available inside Eta templates.

See also

On this page