
Company News
Socket Joins New OpenJS Program to Fund Node.js Security Work
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.
@spintax/core
Advanced tools
Framework-agnostic spintax / text-spinning engine for JS & TS — turn one LLM-drafted {a|b|c} template into endless deterministic variations. Render, validate, extract. Zero-dependency; Node, Workers, browser. Also available as an n8n community node (n8n-n
Framework-agnostic spintax engine for JavaScript / TypeScript — parse, render, validate, extract, analyze, and neutralize spintax templates.
Pairs naturally with LLMs. Have a model draft a spintax template once, then generate unlimited variations on-device — deterministic, free, and offline, with no per-generation API calls. The LLM handles creativity; the engine handles scale.
.d.ts types for both.spintax/core (PHP),
spintax-core (Python), an Object Pascal engine,
Spintax.Core (.NET), and
the Spintax WordPress plugin are all held to the same
behavior contract by a shared golden corpus of 258 fixtures — deterministic verdicts, plural
buckets, conditionals, #set/#def semantics and post-processing agree everywhere, so a
template is an asset you can move between runtimes, not a lock-in.Status: released & stable. Feature-complete — parse / render / validate / extract / analyze / neutralize — with the public API proven by real consumers: a reference Cloudflare Worker, the @spintaxnetbot Telegram bot, the spintax.net playground, and the n8n community node.
npm install @spintax/core
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: [] }
Anywhere one message has to go out many times without reading like a form letter:
context.The core renders a single string per call — batching is a host concern. To emit N variants, call
render N times with different seeds; a seeded call is reproducible, so any variant can be
regenerated later from its seed alone:
const template = '{Hi|Hello|Hey} %name%, {quick|short|small} question about {pricing|billing}';
const variants = [1, 2, 3].map((seed) => render(template, { context: { name: 'Ada' }, seed }));
// → [ 'Hello Ada, quick question about billing',
// 'Hey Ada, quick question about pricing',
// 'Hey Ada, quick question about pricing' ] ← same as seed 2; see the caveat below
Distinct seeds are independent draws, not distinct results — like any sampling they can repeat, and the fewer combinations a template has, the more often they will. If you need N unique variants, dedupe in the host and cap the retries: a template may simply not have N combinations to give.
| Construct | Example | Meaning |
|---|---|---|
| Enumeration | {a|b|c} | pick one (nestable: {a|{b|c}}) |
| Permutation | [a|b|c] | pick N, shuffle, join — [<minsize=1;maxsize=2;sep=", ">a|b|c] |
| Variable | %var% | substitute a context value — inside {…}/[…] the value is spliced as text first, so a ` |
| Local set | #set %v% = value | define a macro — re-picked at every use |
| Local def | #def %v% = value | define a value — picked once per render, held at every use |
| Conditional | {?VAR?then|else} | then if VAR is truthy, else else — inside {…}/[…] it resolves first, so a | in the taken branch separates options and an empty one drops its element |
| Plural | {plural %n%: one|few|many} | grammatical agreement by locale |
| Include | #include "slug-or-id" | embed another template (host-resolved) |
| Comment | /# … #/ | stripped before rendering |
All functions accept a string or a parsed Ast (from parse) as their first argument.
render(input, options?): stringRenders a template to a single string. Lenient — never throws on malformed markup (a bad
block is emitted verbatim with fullwidth braces {…}). Cosmetic post-processing (spacing,
capitalization, URL/email shielding) is on by default.
render(input, {
context?: Record<string, string>, // variable map
seed?: number | string, // deterministic RNG; omit ⇒ random
locale?: string, // plural buckets, e.g. 'ru' (3-form)
includeResolver?: (ref: string) => string | null, // host-injected, synchronous
postProcess?: boolean, // default true; false ⇒ raw pick
maxDepth?: number, // #include / nesting guard (default 20)
});
#include is resolved only when you pass an includeResolver; child templates inherit the
runtime context but not the parent's #set locals. Circular / too-deep includes resolve to
'' (lenient).
validate(input, options?): Diagnostic[]Returns diagnostics. A template is valid ⇔ no diagnostic has severity: 'error'. An
unresolved %var% is a warning, not an error.
validate(input, {
locale?: string, // locale-aware plural-arity verdicts
knownIncludes?: readonly string[], // enables "unknown #include target" errors
knownVariables?: readonly string[], // suppresses undefined-variable warnings
});
// Diagnostic: { severity, code, message, line, column, endLine?, endColumn?, data? }
extract(input): { refs, sets, includes }Variable references (%var%, {?…}, plural counts), #set names, and #include targets.
analyze(input, options?): Analysisextract + validate + a best-effort constructs census ({ enumeration, permutation, variable, conditional, plural, set, include }). The census counts author-visible constructs;
it is not a variant-cardinality promise.
parse(input): AstParses once for reuse. Ast is opaque and versioned — pass it back to the other functions,
do not introspect or persist it across engine versions.
neutralize(value): stringShields data-derived (untrusted) text so it can't be re-interpreted as spintax markup — use it
on any value you inject via context that isn't author-controlled. It is text-safe
(round-trips to literal glyphs in any sink), not HTML/XSS escaping.
render('%bio%', { context: { bio: neutralize('Save {50|60}% today') }, postProcess: false });
// → "Save {50|60}% today" (the braces stay literal, not a random pick)
The pipe is deliberately not shielded: it means nothing outside a construct, and inside one it
is the separator. A neutralized value an author places in {…}/[…] is still split on its |, in
this engine as in every other in the family. Keep such a value out of a construct if it must stay
one option.
n8n-nodes-spintax
(Settings → Community Nodes). One node, five operations: Render, Render Many, a two-output
Validate, and the LLM authoring/repair prompt builders — a Sheets-to-outbox personalization
flow or a full AI authoring loop with no code and no credentials on the node's side.With a seed, render is reproducible within this engine. Cross-engine RNG-sequence parity
with the PHP plugin is a non-goal — only the deterministic behavior (validation verdicts,
plural buckets, conditional truthiness, #set/#def semantics, post-process output) is parity-gated.
@spintax/core ·
n8n-nodes-spintax (the n8n node)/draft)MIT. The Spintax WordPress plugin remains GPL; MIT/Expat is GPL-compatible.
Part of the 301.st toolset. Product home: spintax.net.
FAQs
Framework-agnostic spintax / text-spinning engine for JS & TS — turn one LLM-drafted {a|b|c} template into endless deterministic variations. Render, validate, extract. Zero-dependency; Node, Workers, browser. Also available as an n8n community node (n8n-n
The npm package @spintax/core receives a total of 246 weekly downloads. As such, @spintax/core popularity was classified as not popular.
We found that @spintax/core demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Company News
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.

Security News
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.

Research
/Security News
A malicious Firefox extension fetches its payload after installation to evade detection, steal Google session cookies, and automate account takeover.