Spintax for JavaScript & TypeScript

The spintax engine now ships as a standalone npm package: @spintax/core. Zero dependencies, MIT-licensed, and framework-agnostic — the same parse / render / validate primitives that power the WordPress plugin, now usable anywhere JavaScript runs.

What it is

@spintax/core is an open-source, framework-agnostic spintax engine for JavaScript and TypeScript. It is a companion to the Spintax WordPress plugin — an independent TypeScript implementation of the same syntax, with zero WordPress dependency. One engine, many surfaces.

  • Zero runtime dependencies. Runs unchanged on Cloudflare Workers, Node 18+, and in the browser.
  • ESM-first, dual CJS. Ships .d.ts types for both module systems.
  • MIT licensed. The WordPress plugin stays GPL; MIT/Expat is GPL-compatible, so the two coexist cleanly.
  • Parity-verified against the PHP plugin by a shared golden corpus — not a promise, a machine-checked gate.

Install

npm install @spintax/core

Quick start

import { render, validate, extract } from '@spintax/core';

render('{Hello|Hi|Hey} %name%!', { context: { name: 'Ada' }, seed: 42 });
// → "Hi Ada!"  (deterministic for a given seed; post-processed by default)

validate('{a|b');          // → [{ severity: 'error', code: 'bracket.unclosed', … }]
extract('%title% {?promo?Sale}'); // → { refs: ['title', 'promo'], sets: [], includes: [] }

With a seed, render is fully reproducible. Omit it and every call returns a fresh random pick from the template's space.

The API

A small, sharp core. Every function accepts a raw string or a parsed Ast (from parse), so a consumer can parse once and reuse.

FunctionWhat it does
parse(src)Parse once into an opaque, versioned Ast for reuse — an in-memory perf handle, not a serialization format.
render(input, opts?)Render to a single string. Lenient — never throws on malformed markup. Cosmetic post-processing (spacing, capitalization, URL/email shielding) is on by default.
validate(input, opts?)Return diagnostics. Valid ⇔ no severity:'error'. An unresolved %var% is a warning, not an error. Locale-aware plural verdicts.
extract(input)List variable refs, #set names, and #include targets — powers a two-phase prefetch for async includes.
analyze(input, opts?)extract + validate + a best-effort construct census. Stats layer for tooling.
neutralize(value)Shield untrusted, data-derived text so it can't be re-interpreted as spintax markup. Text-safe, not HTML escaping.

render takes context (the variable map), seed, locale (plural buckets, e.g. ru for the 3-form rule), an optional host-injected includeResolver, a postProcess toggle, and a maxDepth guard for nesting and #include.

The same syntax you already know

It is the full spintax surface — the syntax reference applies verbatim.

ConstructExampleMeaning
Enumeration{a|b|c}pick one (nestable: {a|{b|c}})
Permutation[a|b|c]pick N, shuffle, join — configurable separators
Variable%var%substitute a context value
Local set#set %v% = valuedefine a variable (one line)
Conditional{?VAR?then|else}value-driven branch (guide)
Plural{plural %n%: one|few|many}grammatical agreement by locale (guide)
Include#include "slug-or-id"embed another template (host-resolved)
Comment/# … #/stripped before rendering

Parity with the WordPress plugin

"Independent implementation, but parity where it counts" is the whole point of the package — and it is enforced, not intended. A shared golden corpus of language-neutral fixtures (template, context, locale, seed) → expected is consumed by both the PHP plugin's test suite and this TypeScript suite.

  • Deterministic behavior is parity-gated: validation verdicts, plural buckets, conditional truthiness, #set collapse, and the post-process pipeline assert identical output in both engines.
  • RNG selection is not. Seeded renders are reproducible within an engine; cross-engine random-sequence parity is a deliberate non-goal. You get a valid pick either way, just not the same one.

It is not a line-by-line port of the GPL PHP — it is reimplemented from the behavior contract plus that corpus, which is exactly what keeps it cleanly MIT.

One engine, many surfaces

The package is the shared runtime behind the whole ecosystem — every surface below is a consumer of the same public API, never a private fork:

  • The playground on this site (/play/) renders client-side in your browser.
  • A Cloudflare Worker reference API — validate and preview-render over HTTP.
  • The @spintaxnetbot Telegram bot — paste a template, get validation plus a handful of rendered variations, right in chat.

Because the examples import @spintax/core and nothing else, each one proves the API is usable without polluting it. If it works for the Worker and the bot, it works for your project too.