تخطَّ إلى المحتوى
المحتويات

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

ebnf
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).
  • render arguments 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__, prototype and constructor are 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, so javascript: 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 migrations

File 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):

html
<hk-sections />
<hk-schema>{"sections":[{"type":"hero","settings":{"title":"أهلًا بيك"}},{"type":"featured"}]}</hk-schema>

Section example:

html
<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.

html
<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).

Sneferu language reference · Sneferu | هيكانا