Note
This is one of 199 standalone projects, maintained as part of the @thi.ng/umbrella monorepo and anti-framework.
🚀 Please help me to work full-time on these projects by sponsoring me on GitHub. Thank you! ❤️
Extensible procedural text generation engine with dynamic, mutable state, indirection, randomizable & recursive variable expansions.
The generator works with a simple plain text format, supporting the following features:
Lines starting with #
are comments and completely ignored, and invisible in
the output. No block comments are available.
Text generation and expansion relies on variables, which are defined inline (inside the text given to the generator). New variables can be defined as follows and each variable can define one or more possible values (one value per line!):
[name]
alice
bob
ciara
If more than a single value is given, a semi-random choice will be made for each instance/occurrence of the var...
Variables are case-sensitive (i.e. NAME
is a different var than name
) and
can also be re-defined overridden in different parts of the document.
Variables are referenced via: <name>
. By default, an error will be thrown if a
variable cannot be resolved. Via the missing
option given to generate()
a
default fallback value can be defined, which when given will not trigger an
error in these cases.
Note
The special variable reference <empty>
can be used to produce an empty
string (or used as single value placeholder option inside another variable
definition). This variable can not be redefined.
import { generate } from "@thi.ng/proctext";
const { result } = await generate(`
[name]
alice
bob
[action]
walked
cycled
ran
swam
[place]
office
shop
cafe
lake
[time]
this morning
last night
a week ago
# The actual generated text...
<time> <name> <action> to the <place>.
`);
console.log(result);
// last night alice cycled to the cafe.
Variable values can contain cyclic references and/or references to other variables.
Important
In the case of cyclic refs, you MUST provide at least one non-cyclic option to avoid infinite recursion!
import { generate } from "@thi.ng/proctext";
const { result } = await generate(`
[activity]
walking and <activity_alt>
hiking and <activity_alt>
# the next option is cyclic...
cycling, <activity>
[activity_alt]
sleeping
coding
I enjoy <activity>.
`);
console.log(result);
// I enjoy cycling, walking and coding.
New variables can be declared & initialized via <varname=existing>
. This will
create a new variable varname
with a choice value of variable existing
(which must already be defined).
import { generate } from "@thi.ng/proctext";
const { result } = await generate(`
[name]
Alice
Bob
Charlie
Dora
Let's pick some random names: <hero1=name> and <hero2=name>.
But now we can use them as constants:
<hero1>, <hero1>, <hero1> & <hero2>, <hero2>, <hero2>
`);
console.log(result);
// Let's pick some random names: Dora and Bob.
//
// But now we can use them as constants:
// Dora, Dora, Dora & Bob, Bob, Bob
Hidden assignments
Normally, variable assignments are also causing the chosen value to be emitted
as part of the generated text. Sometimes it's useful to not do so and
suppress/hide output by using the prefix !
as part of the assignment, like so:
import { generate } from "@thi.ng/proctext";
const { result } = await generate(`
[name]
Asterix
Obelix
# hidden assignment
<!hero=name>
My name is <hero>.`);
console.log(result);
// My name is Asterix.
Given a variable assignment like <hero=name>
, we can use dots in the name to
lookup derived variables with their name based on current value(s).
For the following example:
- assume the assignment of
<hero=name>
picked valuea
- now the var lookup
<hero.job>
will look for a var calleda.job
- there's only one option for
a.job
, i.e.astronaut
- the lookup
hero.job.desc
will then look forastronaut.desc
import { generate } from "@thi.ng/proctext";
const { result } = await generate(`
[name]
A
B
[A.job]
astronaut
[B.job]
baker
[astronaut.desc]
shooting for the stars
flying to the moon
[baker.desc]
baking bread
<hero=name> is a <hero.job>, <hero.job.desc>.`);
console.log(result);
// B is a baker, baking bread.
Variable references can be optionally processed via one or more modifiers via
<varname;mod>
or <varname;mod1;mod2...>
. Each modifier is a simple async
function receiving a string and transforming it into another string. These
functions are async to allow modifiers consulting external data
sources/processes...
The following modifiers are provided by default:
uc
: uppercaselc
: lowercasecap
: capitalize
Custom modifiers can be provided via options given to generate()
:
import { generate } from "@thi.ng/proctext";
const DIRECTIONS = {
e: "east",
w: "west",
n: "north",
s: "south",
d: "down",
u: "up",
};
const ROOMS = {
house: {
desc: "very homely",
exits: { e: "garden", u: "rafters" },
},
rafters: {
desc: "pretty dark",
exits: { d: "house" },
},
garden: {
desc: "super lush",
exits: { w: "house" },
},
};
// partially data-driven template
const { result } = await generate(`
[room]
${Object.keys(ROOMS).join("\n")}
You're in the <here=room> (exits: <here;exits;uc>)...
It feels <here;desc> here.
`, {
// custom modifiers (will be added to existing defaults)
mods: {
// produce a list of exits for given room ID
exits: async (id) => Object.keys(ROOMS[id].exits).map((dir) => DIRECTIONS[dir]).join(", "),
// return a room's description
desc: async (id) => ROOMS[id].desc
}
});
console.log(result);
// You're in the house (exits: EAST, UP)...
// It feels very homely here.
The selection of variable choices is determined by the configured PRNG
instance, the number of recent values selected/memorized
and the number of attempts made to pick a new unique choice. All of these can be
configured via options given to generate()
:
import { generate } from "@thi.ng/proctext";
import { SYSTEM } from "@thi.ng/random";
// here we show default options used
generate(`...`, {
rnd: SYSTEM,
maxHist: 1,
maxTrials: 10
});
The maxHist = 1
means that for each variable only the last value picked will
be memorized. If more than 1 value options exists for a variable, successive var
expansions will not result in duplicates, e.g.
[name]
a
b
<name> <name> <name>
...will only ever result in one of these two sequences: a b a
or b a b
.
ALPHA - bleeding edge / work-in-progress
Search or submit any issues for this package
yarn add @thi.ng/proctext
ESM import:
import * as pro from "@thi.ng/proctext";
Browser ESM import:
<script type="module" src="https://esm.run/@thi.ng/proctext"></script>
For Node.js REPL:
const pro = await import("@thi.ng/proctext");
Package sizes (brotli'd, pre-treeshake): ESM: 1.34 KB
- @thi.ng/api
- @thi.ng/arrays
- @thi.ng/checks
- @thi.ng/defmulti
- @thi.ng/errors
- @thi.ng/object-utils
- @thi.ng/parse
- @thi.ng/random
- @thi.ng/strings
Note: @thi.ng/api is in most cases a type-only import (not used at runtime)
One project in this repo's /examples directory is using this package:
Screenshot | Description | Live demo | Source |
---|---|---|---|
Procedural stochastic text generation via custom DSL, parse grammar & AST transformation | Demo | Source |
If this project contributes to an academic publication, please cite it as:
@misc{thing-proctext,
title = "@thi.ng/proctext",
author = "Karsten Schmidt",
note = "https://thi.ng/proctext",
year = 2023
}
© 2023 - 2024 Karsten Schmidt // Apache License 2.0