code, a line, a column
and a fix) and renders it. The Tassyir API checks uploads with it, the
store builder previews with it, the storefront renders with it, and the
tassyir CLI uses it for tassyir block preview: one code, the same answer
everywhere.
Not live yet. Storefront blocks open with the Tassyir app marketplace.
Until then the API refuses [[storefront.blocks]] in a manifest. The
language is published now so that you can write and test blocks.
It has no dependency and reads no file, clock, network or randomness. It
never uses Intl. The same template and data give the same bytes in Node and
in every browser. ESM and CommonJS, named exports only.
Install
Example
A block that shows a product’s rating, read from the app’s data:What you get back
validateBlock, compileBlockTemplate and renderBlock never throw. They
return { ok: true, … } or { ok: false, errors }. Each error has this shape:
Limits
All inBLOCK_LIMITS, the same everywhere:
Render time is counted in steps, never measured with a clock: every text
run, output, tag, loop iteration and condition costs a step, and every filter
and comparison one more per 256 characters or items it touches. The same
template and data always finish, or always stop at the same step. The heaviest
render the budget allows takes a few milliseconds.
How it stays safe
A block runs on the page that holds the store’s order form, so the language is built to make anything else impossible:- Every output is escaped. There is no
rawfilter and no way to turn escaping off. Only Tassyir’s ownstarsfilter makes markup, from fixed parts. - No prototype, method or getter is reachable. The data a render is given is copied into maps and frozen arrays; a template reads own keys and indexes only.
- The HTML is rebuilt, not filtered. Every render’s output is parsed and
written out again from an allowlist (
BLOCK_HTML_ALLOWLIST): allowed elements and attributes only, every value checked and quoted, every text escaped, comments and raw-text elements removed, SVG shapes only inside<svg>. The browser parses that canonical HTML, never the template’s. Sanitising the output again leaves it unchanged. - Only declared data reaches the template. A compiled template keeps its
declaration’s app data keys and setting keys, and a render passes the
template only those, through every alias (
{% assign d = app.data.store %}). - Execution is bounded by counting, never by a clock, so the bound holds on every machine and cannot be raced.
- The box is the host’s. This package decides what the HTML and the
stylesheet may say; it cannot keep a block’s pixels inside its section. The
page that renders a block must contain it:
contain: paint(andisolation: isolate) on the block’s section. The stylesheet check refusesposition: fixedin every spelling, butposition: absolute, transforms and the page’s own utility classes are ordinary CSS that only containment keeps in the box. - Nothing differs between machines: no
Intl, no clock, no randomness, no locale method, no Unicode property table.
javascript: and data: URLs in
their encodings, character-reference tricks, bidi and zero-width characters,
CSS url() and expression(), deep nesting, huge loops), each rendered and
parsed by a spec-compliant HTML parser. Not one forbidden element, attribute
or URL comes out. Every limit stops its runaway case. The browser bundle
renders the corpus to the same bytes as Node.
API
Constants:
BLOCK_LIMITS, BLOCK_ERROR_CODES, BLOCK_FILTERS,
BLOCK_HTML_ALLOWLIST, BLOCK_ROLES, BLOCK_NAMED_ENTITIES,
STOREFRONT_THEME_VARIABLES, BLOCK_PRODUCT_FIELDS, BLOCK_STORE_FIELDS,
BLOCK_FORLOOP_FIELDS, BLOCK_OBJECTS, BLOCK_DATA_OWNERS,
IMAGE_URL_WIDTHS, IMAGE_CDN_ORIGIN, RENDER_STEP_COSTS,
BLOCK_DIALECT_VERSION.