Coming from Liquid
If you write Liquid — or Jinja / Twig, which share the same {{ … }} and {% … %} shapes — Heddle looks close enough to be misleading. The delimiters that matter most mean different things here. This page maps the habits that trip people up to their Heddle equivalents; for the full picture read the Language Reference.
Every Heddle snippet below is a complete template, verified to compile against Heddle 2.0.0 under default options — except the
@partial(){{ sidebar }}snippet, which additionally requires a configuredRootPath/FileNamePostfixand an existingsidebartemplate on disk at compile time (see that section). Where a snippet reads model members it declares adynamicmodel with@model(){{dynamic}}; the Liquid snippets are illustrative and need not compile.
{{ }} is a body, not interpolation
<h1>{{ title }}</h1> {# Liquid / Jinja: interpolates the value #}@model(){{dynamic}}
<h1>{{ Title }}</h1> @* WRONG: {{ }} is a subtemplate body — this prints the braces *@@model(){{dynamic}}
<h1>@(Title)</h1> @* RIGHT: @(...) emits a value *@This is the number-one misread. In Liquid and Jinja {{ x }} interpolates a value; in Heddle {{ … }} delimits a subtemplate body — the block a call or definition renders — so a bare {{ Title }} in text emits the literal braces, not the value. To output a value, use the @(…) output block.
Filters → chains, composed right-to-left
{{ title | emphasize }} {# left-to-right: take title, then emphasize it #}@model(){{dynamic}}
@%
<emphasize>{{ <em>@(Title)</em> }}
%@
@out():emphasize() @* right-to-left: emphasize runs first; @out emits its result *@A Liquid filter pipeline reads left-to-right — the value first, each filter after it. A Heddle chain composes right-to-left: the rightmost call renders first, and each : hands its output to the call on its left as that call's chained value, which the built-in @out() emits. So the transform sits on the right and the output step (@out()) on the left — the same pipeline, read in reverse. The example renders <em>Hello</em>. Note the shape difference: a Heddle transform reads the model directly (here emphasize reads Title), and the built-in @out() is the consumer of the chained value — so to apply several transforms you compose definitions rather than piping one scalar through a long chain.
include / render → @<< vs @partial()
{% include 'sidebar' %} {# or {% render 'sidebar' %} #}@model(){{dynamic}}
@partial(){{ sidebar }} @* compiled when the enclosing template compiles; rendered inline at run time *@Liquid's include and render both splice in another template's output. Heddle splits the job: @partial() resolves and compiles its target when the enclosing template compiles, then renders that template's output inline at run time (the closest match), while @<<{{ file }} is a compile-time definition import for sharing layouts and reusable blocks. Reach for @partial() to embed rendered output, @<< to share definitions. Because the target is read at compile time, this snippet needs more than the defaults to compile: set TemplateOptions.RootPath and FileNamePostfix, and have a sidebar template on disk — a missing partial is a compile error here, not a render-time one (unlike Liquid's include).
for / forloop.index → @list + @out()
{% for a in articles %}
{{ forloop.index }}. {{ a.title }}
{% endfor %}@model(){{dynamic}}
@list(Articles){{ @out(). @(Title) }}In Liquid the loop variable and forloop.index are separate names. Heddle's @list makes each element the current model — so @(Title) is the element — and exposes the index on the chained channel, which @out() emits. Note the index is zero-based, where Liquid's forloop.index starts at 1.
Tags → extensions
{% if featured %}<b>Featured</b>{% endif %}
{% assign x = 1 %}@model(){{dynamic}}
@if(IsFeatured){{ <b>Featured</b> }}Liquid's {% … %} tags — if, for, assign, and any custom tag — are a separate syntax. Heddle has no tag syntax at all: every construct is an extension call, whether a built-in like @if / @list or your own @name() C# class, so control flow and custom logic share one form. See Built-in Extensions and Writing Custom Extensions.
What to unlearn: whitespace-control dashes
{%- assign x = 1 -%}
{{- title -}} {# the dashes strip surrounding whitespace #}@model(){{dynamic}}
@using(){{System.Linq}}@\
<h1>@(Title)</h1>Liquid trims whitespace with in-delimiter dashes ({%- -%}, {{- -}}). Heddle has no in-delimiter form: use @\ to eat the following run of whitespace (here suppressing the declaration line's trailing newline), or set TemplateOptions.TrimDirectiveLines — on by default since 2.0 — so a whole-line directive swallows its own line without any marker.