# Spintax Syntax Reference Complete reference for the spintax format, also called spin syntax: every construct, the rules it follows and the traps it hides. ## Enumerations {a|b|c} Randomly selects **one** option from the list. ``` {option1|option2|option3} ``` ### Examples ``` {blue|grey|clear} {|free|paid} plan ← empty option = sometimes nothing {Acme {Pro|Lite}} ← nested enumerations {order {|#42-A} confirmed} ← nesting with empty option ``` ### Rules - Delimiters: `{` and `}` - Separator: `|` - Supports nesting to arbitrary depth - Empty options are valid (produce empty string) - Resolution is from the innermost expression outward ## Permutations \[a|b|c\] Selects N elements, shuffles them, and joins with separators. ### Simple permutations All elements included, space-separated: ``` [1|2|3|4] ``` Output examples: `1 4 3 2`, `2 3 4 1`, `3 2 4 1` ### With separator Uniform separator specified in `< >` at the start: ``` [<, > 1|2|3|4] ``` Output examples: `2, 1, 4, 3` · `4, 3, 2, 1` **Important:** No space between `[` and ``. ### Per-element separators Each option can have its own separator defined with `` before the preceding `|`. The separator travels with its element during shuffle. ``` [<, > 1|2|3 < and >|4] ``` Output examples: `1, 3, 2 and 4` · `3, 1, 2 and 4` **Auto-spacing:** Word separators like `` or `` are automatically padded with spaces, so `` produces `and` . Punctuation separators (`<,>`) are not padded. Two exceptions: a separator written only in Han, Hiragana, Katakana, Thai, Lao, Khmer or Myanmar script joins bare (`<和>` gives `a和b`), and under the `ar` locale `و` and `ف` attach to the next Arabic word (`الكازينو والبث`), as `ו` does under `he`. ### Permutations with combinations Configurable min/max element count and separators: ``` [ apple|plum|orange|apricot] ``` Output examples: `apple, plum and orange` · `apple and apricot` · `orange` ### Configuration parameters | Parameter | Default | Description | | --- | --- | --- | | `minsize` | count of all | Minimum number of elements to pick | | `maxsize` | count of all | Maximum number of elements to pick | | `sep` | `" "` (space) | Separator between non-final items | | `lastsep` | same as `sep` | Separator before the last element | ### Permutation rules - Delimiters: `[` and `]` - Config block `<...>` must immediately follow `[` - Config parameters are semicolon-separated - String values in config are quoted: `sep=", "` - Enumerations and permutations can be nested inside options - HTML elements can be options - `sep` joins everything before the final pair, `lastsep` joins that pair: with two items selected only `lastsep` appears, with one neither does ## Variables `%var%` Defines a reusable variable that is substituted wherever it appears. Two directives declare one, and the choice is not cosmetic: `#set` is a macro — its value is substituted afresh at every reference, so any spintax inside it re-rolls; `#def` rolls the value once per render and hands that one result to every reference. (One render is one output; the same seed reproduces it.) While the value is a plain literal the two are identical; the difference appears the moment the value contains a choice. ``` #set %VARIABLE_NAME% = value or spintax structure #def %VARIABLE_NAME% = value or spintax structure ``` ### Examples ``` #set %name% = John #set %greeting% = {Hello|Hi|Hey} #set %items% = [ apples|oranges|bananas] Some text with %name% and %greeting%, also %items%. And once more, %greeting% — a second reference. /# %greeting% above may differ between the two references — #set re-rolls. #def picks once and keeps it: #/ #def %tone% = {friendly|warm|upbeat} A %tone% intro, and a %tone% outro — always the same word. ``` ### Variable rules - `#set` and `#def` must start at the beginning of a line - Variable names are enclosed in `%`: `%name%` - Variable names are alphanumeric + underscore, and references are case-insensitive: `%Tone%` and `%tone%` are one variable - Values can contain any spintax syntax (enumerations, permutations, other variables) - `#set` is a macro: it expands when referenced, not when defined, and its value — including any spintax inside it — is substituted afresh and re-rolled at every reference - `#def` resolves its value once per render and holds that result everywhere. That is what keeps repeated words in agreement: a noun and the case forms built from it, a count feeding a `{plural}` block, any phrase whose repeats must match word for word - `#def` makes one variable consistent with itself; it does **not** correlate two variables. `#def %Noun%` and `#def %NounGen%` are two independent rolls and can land on different words — forms that must agree have to come from a single roll: one `#def` stem that every case form references, or synonyms that decline alike with the ending written outside the definition - A name is defined once. A second definition of the same name is reported as `definition.duplicate-name` and the render still goes ahead: between two of the same directive the later one wins, and when a `#set` and a `#def` share a name the `#def` wins whichever of them came first - A reference with no definition prints itself: `%missing%` stays in the output rather than disappearing - Neither directive crosses an `#include`: an included template does not see the parent's locals, and its own do not leak back. A rolled form reaches a child only as a runtime variable - `#set` and `#def` lines are stripped from output - **Deep dive:** see the [Variables guide](https://spintax.net/docs/variables.md) for scopes and the reroll gotcha, and [Grammar-safe synonymization](https://spintax.net/docs/grammar-safe-spintax.md) for case families ### Variable scopes A host can supply variables from more than one place. When the same name exists in several of them, the strongest wins: 1. **Runtime variables** (strongest) — what the host passes into the render call: `context` in `@spintax/core`, shortcode attributes in the WordPress plugin: `[spintax slug="greeting" name="Alice"]` 2. **Local variables** — defined with `#set` or `#def` inside the template 3. **Global variables** (weakest) — host-wide defaults, such as the plugin's Settings page ## Conditionals `{?VAR?then|else}` Conditionals are **the language's own construct** — nothing like them existed in the GTW prototype. Where `{a|b}` is a uniform random pick that ignores variables, `{?VAR?then|else}` picks based on whether `%VAR%` has a value. Use it for value-driven choices — show a free-tier line only when a free tier exists, render a pro-features block only when the user is on a paid plan, hide a CTA that does not apply. The pre-pass runs before `%var%` expansion and before the random branch picker, so a falsy branch is fully discarded — nothing inside it is evaluated. ### Forms ``` {?VAR?then} ← truthy ⇒ then; falsy ⇒ empty {?VAR?then|else} ← truthy ⇒ then; falsy ⇒ else {?!VAR?then|else} ← inverted ``` ``` {?HasFreeTier? — free tier available since %founded%|, trusted since %founded%} ``` ### Truthy and falsy The rule is deliberately simpler than JavaScript — **truthy = at least one non-whitespace character**: | Value of `%VAR%` | Truthy? | | --- | --- | | not declared | falsy | | empty string | falsy | | whitespace only | falsy | | `"0"`, `"false"` | truthy (non-empty) | | any other text or HTML | truthy | ### Conditional rules - Variable names follow the same regex as `%var%` (case-insensitive) - The `!` prefix inverts the check: `{?!VAR?missing}` - The first depth-0 `|` separates `then` from `else`; later top-level `|` stays literal in `else` - Nested conditionals evaluate outer-first — falsy branches short-circuit - Composite logic (`&&`, `||`, comparisons) is not supported — pre-compute a guard variable in the assembler - Malformed forms (`{??yes}`, `{?VAR}`) never throw — the playground flags them as warnings - **Deep dive:** see the [Conditional spintax guide](https://spintax.net/docs/conditional-spintax.md) for worked examples and anti-patterns ## Plurals `{plural %n%: language|languages}` Picks the grammatically correct word form for a number. The count goes before the colon, the forms after it, separated by `|`. The form is chosen by the **render locale**, not by the template — so how many forms you must supply depends on that locale. English needs two, Russian needs three. ``` {plural %n%: form1|form2} ← 2-form locale (en, de, es…) {plural %n%: form1|form2|form3} ← 3-form locale (ru, uk, sr…) {plural %n%: zero|one|two|few|many|other} ← 6-form locale (ar) ``` ``` #def %LangCount% = 5 supports %LangCount% {plural %LangCount%: language|languages} ← supports 5 languages ``` ### Forms per locale The locale is matched on its language subtag, so `ru-RU` and `ru` behave identically: | Locale | Forms | Selected by | | --- | --- | --- | | `ru`, `uk`, `be`, `sr`, `hr`, `bs` | 3 | 1 · 2–4 · 5 and up | | `ar` | 6 | 0 · 1 · 2 · last two digits 3–10 · last two digits 11–99 · everything else (100–102, 200–202…) | | every other locale, incl. `en` | 2 | exactly 1 · everything else | Supply the wrong number of forms and the engine reports `plural.arity` and leaves the block visible with fullwidth braces — a silent wrong plural never ships. ### Plural rules - The opener is literal, including the space: `{plural` . `{plural: x}` and `{pluralN: x}` are not plural blocks - The colon is mandatory — it separates the count from the forms - The count is a `%Var%` reference or a literal integer; variables in the count are substituted before the form is chosen - Negative counts use the absolute value; `0` takes the "everything else" form (under `ar`, the first form, its own) - **A count variable must be `#def`, not `#set`** — `#set` is a macro, so a value like `{1|4|9}` is still unresolved spintax when the plural is decided and the whole block renders empty. The playground flags this as `plural.count-macro` - A non-numeric or undefined count erases the block rather than guessing - **Deep dive:** see the [Plural spintax guide](https://spintax.net/docs/plural-spintax.md) for the Russian 3-form rules and worked examples ## Includes `#include` Embeds another template at the directive's position. `#include` is the one construct the engine cannot answer by itself: it keeps no template store, so the host supplies a resolver that turns a reference into template text. Where no resolver is installed — the playground and the MCP server on this site, both deliberately — the directive is inert and stays in the output as literal text. ``` #include "hero-text" /# wrong: text before the directive on the same line leaves it literal #/ Intro: #include "hero-text" ``` ### Include rules - The directive must occupy the whole line. Leading whitespace is fine; text after the reference is not — `Text #include "hero"` stays literal - The reference is in double quotes; single quotes or no quotes are not the directive - Resolution belongs to the host: the WordPress plugin resolves by template slug or numeric ID, a JavaScript host passes an `includeResolver` - With no resolver the line stays literal in the output; when the resolver has no such template the line is **removed** instead — an unknown target costs you the block silently - Included templates can contain their own variables and spintax, and their own `#include` - Includes are resolved after the parent's enumerations and permutations have been picked, so an include inside the branch that won is embedded — one inside a discarded branch never happens - Chains work (a template includes a template that includes another); a template that includes itself, directly or through a cycle, is cut at the first repeat — with no error and no diagnostic - Child templates inherit global and runtime variables but **not** parent's `#set` / `#def` locals, and their own do not leak back - An include cannot be a definition's value: `#def %x% = #include "y"` is rejected as `def.include-in-value` - Before rendering, `validate()` flags an unknown target only when the host passes the list of known references; `extract()` returns the references a template needs, which is how a host prefetches them - **Deep dive:** see the [Template composition guide](https://spintax.net/docs/template-composition.md) for the assembler pattern most pipelines use instead ## Comments `/#...#/` Text between comment markers is stripped from the output before any other processing. ``` /# This is a comment section. It can span multiple lines. It won't appear in output. #/ ``` ### Comment rules - Start delimiter: `/#` - End delimiter: `#/` - Can span multiple lines - Cannot be nested - Removed before any other processing - The opposite is the [verbatim span](#verbatim) `/!...!/`: it keeps its text instead of dropping it ## Verbatim span `/!...!/` Text between the verbatim markers reaches the output exactly as written. Nothing inside is syntax, and the post-processing does not touch it. Use it for text that looks like spintax but is not: a Markdown link, a macro another tool expands, BBCode, or a spintax example you want to show rather than render. The span is new in `@spintax/core` 0.14.0. The other engines add it as each one releases it. ``` Docs: /![Spintax](https://spintax.net)!/ → Docs: [Spintax](https://spintax.net) Write /!{Hello|Hi}!/ to get two variants. → Write {Hello|Hi} to get two variants. Pull from /!#file[D:\kw.txt,1,S]!/ here. → Pull from #file[D:\kw.txt,1,S] here. ``` ### Verbatim rules - Start delimiter: `/!`; the first `!/` after it closes the span - Can span multiple lines and cannot be nested - Inside a span, `{`, `[`, `|`, `%var%` and `#set` are plain text - Works in a `#set` or `#def` value, so one span can be reused - Post-processing runs around the span as usual, but never changes, capitalizes or trims the text inside it - An empty span `/!!/` is removed - Whichever of `/#` and `/!` comes first wins: a comment can hide a `/!`, and a span can keep a `/#` - A `/!` with no closer is plain text, and the validator warns `verbatim.unclosed` - Only the template is read this way: a `/!` in a runtime variable value is two characters. To keep untrusted data literal, pass it through `neutralize()`. It does not shield `|`, so keep such a value outside `{…}` and `[…]`, and it does not stop the [post-processing](#post-processing): for byte-for-byte output, also pass `postProcess: false` ## Nesting All syntax elements can be nested within each other to arbitrary depth: ``` {option1|[<, > sub1|sub2|sub3]|option3} [ {red|blue} apples|{big|small} oranges|bananas] #set %var% = {a|[b|c]} ``` ## Post-Processing The engine applies automatic text correction after generation: 1. Shield URLs (a bare `https://` too), emails, domains, decimals, abbreviations, and tokens that start with a dot (`.NET`, `.gitignore`, `.xlsx`) from capitalization and spacing 2. Collapse duplicate spaces and tabs 3. Remove whitespace before punctuation (`,` `.` `!` `?`) 4. Add space after punctuation where missing, but never between a mark and the quote or bracket that closes it (`"Really?"`, `(etc.)`) 5. Capitalize first letter of the output (skipping HTML tags) 6. Capitalize after sentence-ending punctuation 7. Capitalize after block-level HTML tags 8. Capitalize after line breaks 9. Restore shielded placeholders Capitalization follows the render locale: under `tr` and `az` a sentence that opens on `i` starts with `İ`. A [verbatim span](#verbatim) is skipped by every step. ## Syntax Summary | Feature | Syntax | Behavior | | --- | --- | --- | | Enumeration | `{a|b|c}` | Pick one random option | | Permutation | `[a|b|c]` | Pick N, shuffle, join | | Separator | `[ a|b|c]` | Permutation with uniform separator | | Per-element sep | `[<, > a|b |c]` | Permutation with custom separators | | Combinations | `[ a|b|c]` | Permutation with min/max count | | Variable | `#set %var% = {a|b}` | Re-substituted at every reference — the spintax inside re-rolls | | Variable (roll once) | `#def %var% = {a|b}` | One roll per render, held everywhere — how repeated words and their endings stay in agreement | | Conditional | `{?VAR?then|else}` | Render `then` if truthy, `else` if falsy | | Plural | `{plural %n%: language|languages}` | Agree the word form with the number, by locale | | Include | `#include "slug"` | Embed another template — the host resolves the reference | | Comment | `/#...#/` | Stripped from output | | Verbatim | `/!...!/` | Output exactly as written | The language outgrew its prototype, [Generating The Web](https://spintax.net/spintax-editor.md) (GTW) — templates written for GTW still run unchanged. --- [Open in playground](https://spintax.net/play/) [All Docs](https://spintax.net/docs.md) [GTW History](https://spintax.net/spintax-editor.md#gtw) [GitHub](https://github.com/investblog/spintax) --- Source: https://spintax.net/docs/syntax