Reference
Sneferu language reference
A theme is a folder of .sneferu templates (HTML with hk-* tags, {{ }} output and {% %} directives), a theme.json manifest, locale files and assets. sneferu build compiles it to one JSON artifact; the runtime renders that artifact to HTML. Templates cannot run code: the only way to produce output is the grammar below.
Grammar
template = { node } ;
node = text | comment | interp | directive | element | schema ;
comment = "" ; (* dropped at compile time *)
interp = "{{" expr "}}" ;
directive = "{%" ( if | elsif | else | endif | for | endfor | render ) "%}" ;
if = "if" expr ;
elsif = "elsif" expr ;
else = "else" ;
endif = "endif" ;
for = "for" ident "in" expr [ "limit:" integer ] ;
endfor = "endfor" ;
render = "render" string [ "," ident ":" expr { "," ident ":" expr } ] ;
element = "<" name { attr } ( "/>" | ">" { node } "</" name ">" ) ;
attr = name [ "=" ( quoted | bare ) ] ; (* quoted values may contain {{ expr }} *)
schema = "<hk-schema>" json "</hk-schema>" ;
expr = or { "|" filter } ;
filter = ident [ ":" primary { "," primary } ] ;
or = and { "or" and } ;
and = not { "and" not } ;
not = [ "not" ] cmp ;
cmp = primary [ ( "==" | "!=" | ">" | "<" | ">=" | "<=" ) primary ] ;
primary = string | number | "true" | "false" | "nil"
| "(" expr ")"
| lookup { tail }
| ident { tail } ;
lookup = lookupname "(" [ literal { "," literal } ] ")" ; (* literal arguments only *)
tail = "." ident | "[" integer "]" ;Notes:
- Strings use
'single'or"double"quotes. There is no arithmetic, no assignment and no method calls (a.b()is rejected). - Comparison operators
>,<,>=,<=are true only when both sides are numbers.==and!=are strict. - Truthiness (in
{% if %},{% elsif %},and,or,not):nil,false,0, an empty string''and an empty array[]are false; everything else (including{}and'0') is true.{% if collection('x').products %}is therefore false for an empty collection. {% else %}may appear once per{% if %}, after every{% elsif %}(SN1109 otherwise).renderarguments are separated by commas outside quotes and parentheses, so{% render 'tag', label: 'a, b' %}passes one string.- Element and directive nesting is limited to 64 levels (SN1108).
- Missing data never throws: it evaluates to nothing and renders as an empty string. Properties named
__proto__,prototypeandconstructorare always undefined, and only own properties are read. {{ }}output is escaped for its position: text, attribute, or URL (URLs go through a safe-URL check, sojavascript:becomes#).- An attribute may mix text and expressions:
class="card {{ section.settings.tone }}". - Tag and attribute names are lower-cased.
Directives
| Directive | Example |
|---|---|
{% if expr %} / {% elsif expr %} / {% else %} / {% endif %} |
{% if product.compare_at > product.price %}<hk-badge text="{{ 'sale' | t }}" />{% endif %} |
{% for x in expr [limit:n] %} / {% endfor %} |
{% for p in collection('featured').products limit: 8 %}{{ p.title }}{% endfor %} |
{% render 'snippet', key: expr %} |
{% render 'price-tag', amount: p.price %} renders snippets/price-tag.sneferu with amount in scope |
<hk-slot name="content" /> |
layout and templates only: where the template (in a layout) or the sections (in a template) go |
<hk-sections /> |
shorthand for the sections slot, used in templates |
<hk-schema>{json}</hk-schema> |
declares settings (sections, blocks) or default sections (templates) |
Loops: the default limit is 50 iterations, the maximum is 250. A limit below 1 or above 250 is an error (SN2007). A page may run at most 2,000 loop iterations in total.
Snippets: {% render %} may nest up to 8 deep. Snippets may not include each other in a cycle (a renders b, b renders a, or a renders itself): that is a compile error (SN3027, snippet include cycle: a → b → a). Every node rendered (text, markup, render, if, for, slot, component) counts against the page's node budget, so wide fan-out (a snippet rendering another several times, several levels deep) stops at the budget instead of growing exponentially.
Blocks
A section lists its blocks with a loop over section.blocks and renders each with hk-block:
{% for block in section.blocks %}<hk-block of="{{ block }}" />{% endfor %}Each block is rendered from blocks/<type>.sneferu with block in scope (block.id, block.type, block.settings.*) and wrapped in <div data-hk-block="<id>">. A block whose type is not in the theme is skipped with SN4012.
Scope
| Name | Contains |
|---|---|
store |
the store (e.g. store.name) |
settings |
the theme-level settings declared in theme.json settings: defaults filled in and stored values coerced like section settings |
section |
in sections: section.id, section.type, section.settings.*, section.blocks |
block |
in blocks: block.id, block.type, block.settings.* |
locale, dir |
ar / en, rtl / ltr |
forloop |
inside {% for %}: forloop.index (from 1), forloop.first, forloop.last |
loop variable, render arguments |
as named by the template |
x.size works on strings and arrays.
Filters
Apply with |, arguments after : ({{ title | truncate: 40 }}).
| Filter | Input | Args | Example |
|---|---|---|---|
money |
price in minor units (piastres, cents) | none | EGP: 129000 gives 1,290.00 ج.م (Arabic) or EGP 1,290.00 (English). Other store currencies use the ISO code: 1,290.00 USD (Arabic, code after) or USD 1,290.00 (English, code before). Always two decimals, Western digits, , grouping. |
img |
image reference | optional width | {{ p.image | img: 400 }} gives a sized image URL |
date |
ISO date string | none | 2026-10-10 gives 10 October 2026 (English) or the Arabic date with Western numerals |
t |
translation key | none | {{ 'shop_now' | t }} gives the text from locales/<locale>.json, falling back to the default locale, then the key |
escape |
any | none | {{ x | escape }} converts to a string (output is escaped anyway) |
upcase / downcase |
string | none | abc gives ABC |
truncate |
string | max length (default 50) | {{ 'abcdefgh' | truncate: 4 }} gives abc… |
default |
any | fallback | {{ x | default: 'n/a' }} when x is nil or empty |
size |
array or string | none | {{ items | size }} gives 3 |
first / last |
array | none | {{ items | first }} |
join |
array | separator (default , ) |
{{ tags | join: ' / ' }} gives a / b |
json_attr |
any | none | JSON for a data-* attribute |
Lookups
Called like functions with literal arguments only.
| Lookup | Returns |
|---|---|
collection('handle') |
the collection (use .products); an empty collection if missing |
product('handle') |
the product, or nothing |
t('key') |
the translation (same fallback as the t filter); the key if missing |
settings('key') |
one theme setting |
asset_url('file.css') |
URL of a file in assets/ |
A lookup may be followed by property access: collection('featured').products.
Allowed elements and forbidden attributes
Plain elements are checked against an allow-list; anything not on it is an error (SN2001), including custom elements.
- Sectioning and grouping:
address article aside blockquote dd details div dl dt figcaption figure footer h1–h6 header hgroup hr li main menu nav ol p pre search section summary ul - Text:
a abbr b bdi bdo br cite code data del dfn em i ins kbd mark meter progress q rp rt ruby s samp small span strong sub sup time u var wbr - Media:
audio img picture source track video - Tables:
caption col colgroup table tbody td tfoot th thead tr - SVG presentation:
svg g path circle ellipse line polyline polygon rect text tspan defs lineargradient radialgradient stop clippath mask pattern symbol use title desc image
Never allowed (the message names these explicitly): script, style, iframe, object, embed, base, link, meta, form, animate, set, animatemotion, animatetransform, foreignobject, plaintext, xmp, noembed, noscript, frameset, frame, template, slot, portal. Form controls (button, input, select, textarea) are not on the list either; use hk-button for actions.
Attributes (SN2002): srcdoc, formaction, attributename, xml:base, every attribute starting with on (onclick, ...), and names outside [a-z_:][a-z0-9_:.-]*.
Also rejected:
style="..."containing{{ }}(SN2009). Use classes, tokens or component props.- A static URL that is unsafe (SN2010).
URL attributes are href, every *:href (xlink:href, ...), src, action, poster, srcset and formaction. Their dynamic values are always passed through the safe-URL filter at render time, and static values must already be safe (SN2010). Styling lives in assets/*.css and in component props.
Because a theme is untrusted input, the runtime re-validates the compiled artifact before rendering and refuses anything that does not pass (SN9xxx). That check also requires every static HTML chunk to match the compiler's own output grammar: escaped text, and complete tags <name attr="value"> / </name> with allow-listed names and double-quoted values (no comments, <!…>, <?…>, CDATA, unquoted values, or markup inside <title>). Attributes are checked by name (on*, forbidden names) and by value (javascript:, vbscript:, data:text/html, unsafe URLs, including srcset candidates); text itself is never pattern-matched, so prose such as "Buy one = get one free" or "javascript:" is fine. The compiler runs the same check on its own output, so a theme that compiles always renders (SN9014 otherwise; e.g. style="background:url(javascript:…)").
Limits
| Limit | Value |
|---|---|
| Loop default / maximum limit | 50 / 250 |
| Loop iterations per page | 2,000 |
| Snippet and block include depth | 8 |
| Render nesting depth | 64 |
| Rendered nodes per page | 20,000 |
| Output size per page | 1,500,000 characters |
| Files per theme | 400 |
| Theme size | 25 MB |
| One asset | 2 MB |
| One template | 300 KB |
When a render budget is exceeded, the offending section degrades to a comment and the rest of the page is kept (SN4001). SN4002 and SN4003 are reported at most once per page.
File roles and layout
theme.json manifest
layout/theme.sneferu the page frame (header, <hk-slot name="content" />, footer)
templates/*.sneferu one per page type; templates/index.sneferu is required
sections/*.sneferu reusable page parts with settings and blocks
blocks/*.sneferu items inside a section, rendered with hk-block
snippets/*.sneferu partials used with {% render %}
assets/ css, js, images, fonts (png jpg webp avif gif svg woff woff2 ttf otf ico)
locales/<xx>.json flat key to text maps (ar.json, en.json); every value must be a string (SN3025)
migrations/*.json settings migrationsFile and folder names are lower-case letters, digits, ., _, -. Symlinks, .. and absolute paths are rejected (SN3002).
| Role | What it does | Starter example |
|---|---|---|
| Layout | wraps every page; the content slot receives the template | layout/theme.sneferu |
| Template | page type; usually just <hk-sections /> plus a <hk-schema> listing default sections |
templates/index.sneferu |
| Section | rendered inside <div data-hk-section="<id>">; has a <hk-schema> |
sections/hero.sneferu |
| Block | a repeatable item inside a section | blocks/chip.sneferu |
| Snippet | a partial, no schema (the starter has none) | snippets/<name>.sneferu |
Template example (default sections for the page):
<hk-sections />
<hk-schema>{"sections":[{"type":"hero","settings":{"title":"أهلًا بيك"}},{"type":"featured"}]}</hk-schema>Section example:
<hk-section background="theme:secondary" padding-y="96" mobile-padding-y="48">
<hk-heading text="{{ section.settings.title }}" level="h1" size="56" mobile-size="32" />
<hk-button label="{{ t('shop_now') }}" href="/products" />
</hk-section>
<hk-schema>{"name":{"ar":"واجهة","en":"Hero"},"version":1,"settings":{
"title":{"type":"text","label":{"ar":"العنوان","en":"Title"},"default":"Hello","maxLength":80}}}</hk-schema>A section or block schema needs name with both ar and en (SN3030). A section schema may also carry blocks ({ "allowed": [...], "max": n }) and presets. A rendered page wraps the whole output in <div data-hk-theme="<id>" dir="rtl|ltr" lang="ar|en">. lang is the render locale when it is two lower-case letters (otherwise the theme's default locale, otherwise ar), and dir is derived from it: rtl for ar, ltr for everything else. Components inside a section get ids s-<sectionId>-c<n> (CSS class sn-s-<sectionId>-c<n>), so re-rendering one section produces the same ids it had on the full page; components in the layout use c<n>.
theme.json
Required: id (3-41 chars: lowercase letters, digits, dashes, starting with a letter), name and description ({ar,en}), version (x.y.z), author, category, engine: "hk2", engineCompatibility (^2.0.0 or exact; the current engine is 2.0.0), defaultLocale, locales (must include the default), settings (a field schema), and tokens.colors with primary, secondary, accent, background, text as #RRGGBB.
Field types (<hk-schema> settings)
Each field is { "type", "label": {"ar","en"}, "default"?, "required"?, "min"?, "max"?, "step"?, "options"?, "responsive"?, "visible_if"?, "maxLength"? }.
| Type | Value | Coercion of stored setting values |
|---|---|---|
text |
short string | string, cut to maxLength |
textarea |
long string | as text |
rich_text |
string | as text |
number |
number | parsed, clamped to min/max; default if not numeric |
range |
number (slider) | as number |
boolean |
true/false | true/"true"/"yes"/1 and the opposites; else default |
select |
one of options |
default if not in options |
color |
#hex or a theme:<token> |
default if neither |
image, video, link, icon |
string reference | string, else default |
product, collection |
handle string | string, else default |
spacing, typography |
string | string, else default |
Coercion (clamping, defaults, select fallback, colour checks, maxLength) applies only to section and block settings declared in <hk-schema>: a bad stored value falls back to the default instead of breaking the page. Props on hk-* tags are not coerced by the schema; they receive the evaluated value as-is and each tag validates its own props (for example hk-product-grid accepts an array or {products}).
Responsive attributes
A prop declared responsive accepts tablet- and mobile- prefixed variants; the unprefixed attribute is the base. mobile-columns="2" sets columns on mobile. Using a prefix on a prop that is not responsive is SN2004.
<hk-product-grid products="{{ collection('featured').products }}" limit="8" columns="4" tablet-columns="2" mobile-columns="2" />Built-in tags
Layout: hk-section, hk-container, hk-stack, hk-grid. Typography: hk-heading, hk-text, hk-badge. Media: hk-image. UI: hk-button, hk-card. Commerce: hk-price, hk-product-card, hk-product-grid. Passthrough attributes on tags: class, id, role, title, data-*, aria-*. Every built-in tag writes them on its root element: class is appended to the component's own sn-… class (reduced to letters, digits, -, _), id becomes one such token, and role, title, data-*, aria-* are attribute-escaped. data-hk-* is reserved for the platform and dropped. Unknown tags and props get a "did you mean" hint (SN2003, SN2004); required props are checked (SN2005) and child rules enforced (SN2006).
