@thi.ng/proctext
Version:
Extensible procedural text generation engine with dynamic, mutable state, indirection, randomizable & recursive variable expansions
164 lines (159 loc) • 5.13 kB
JavaScript
import { peek } from "@thi.ng/arrays/peek";
import { isString } from "@thi.ng/checks/is-string";
import { DEFAULT, defmulti } from "@thi.ng/defmulti/defmulti";
import { defError } from "@thi.ng/errors/deferror";
import { mergeDeepObj } from "@thi.ng/object-utils/merge-deep";
import { defContext } from "@thi.ng/parse/context";
import { defGrammar } from "@thi.ng/parse/grammar";
import { SYSTEM } from "@thi.ng/random/system";
import { pickRandomUnique } from "@thi.ng/random/unique-indices";
import { capitalize, lower, upper } from "@thi.ng/strings/case";
const CyclicReferenceError = defError(
() => "error expanding variable",
(id) => `: "${id}" (cycle detected)`
);
const UnknownModifierError = defError(() => "unknown modifier");
const UnknownVariableError = defError(() => "unknown variable");
const DEFAULT_MODIFIERS = {
cap: capitalize,
uc: upper,
lc: lower,
withArticle: (x) => (/[aeiou]/i.test(x[0]) ? "an " : "a ") + x,
isAre: (x) => x + (x.endsWith("s") ? " are" : " is")
};
const GRAMMAR = defGrammar(`
# main parser rule
main: <START> ( <def> | <comment> | <binding> | <word> )* <END> => hoist ;
# variable definition
def: <WS0> <LSTART> '['! <id> ']'! <DNL> (<comment> | <value>)+ <DNL> ;
id: ( <ALPHA_NUM> | [.\\u002d] )+ => join ;
value: .(?+<DNL>) => join ;
# variable bindings
binding: '<'! (<setvar> | <var>) '>'! => hoist ;
var: <id> <modifier>? ;
modifier: <modid>+ ;
modid: ';'! <ALPHA_NUM>+ => join ;
setvar: '!'? <id> '='! <var> ;
# line comment
comment: <LSTART> '#' .(?+<DNL>) => discard ;
# verbatim text
word: ( <ALPHA_NUM> | [ ?!.,'"@#$%&(){};:/*=+\\u002d\\u005b\\u005d\\u000a] )+ => join ;
`);
const __parse = (src, opts) => {
const ctx = defContext(src, opts);
return { result: GRAMMAR.rules.main(ctx), ctx };
};
const generate = async (src, ctx) => {
try {
const { result, ctx: parseCtx } = __parse(src.trim());
if (result) {
const $ctx = mergeDeepObj(
{
vars: {},
mods: DEFAULT_MODIFIERS,
rnd: SYSTEM,
maxHist: 1,
maxTrials: 10
},
ctx
);
const acc = [];
await __transformScope(parseCtx.root, $ctx, acc);
return {
type: parseCtx.done ? "success" : "partial",
result: acc.join(""),
ctx: $ctx
};
} else {
return { type: "error", err: new Error(`parse error`) };
}
} catch (e) {
return { type: "error", err: e };
}
};
const __transformScope = defmulti(
(x) => x.id,
{},
{
// fallback handler for unknown tree nodes (usually a grammar error)
[DEFAULT]: async (scope) => {
throw new Error(`missing impl for scope ID: ${scope.id}`);
},
// handler for processing the root node (just traverses children)
root: async ({ children }, ctx, acc) => {
if (!children) return;
for (let x of children[0].children)
await __transformScope(x, ctx, acc);
},
// handler for a new variable definition and its possible values
def: async ({ children }, ctx) => {
ctx.vars[children[0].result] = {
opts: children[1].children.map((x) => x.result),
history: []
};
},
// handler for variable lookups
var: async (scope, ctx, acc) => {
acc.push(await __expandVar(scope, ctx));
},
// handler for variable assignments
setvar: async ({ children }, ctx, acc) => {
const choice = await __expandVar(children[2], ctx);
ctx.vars[children[1].result] = {
opts: [choice],
history: [choice]
};
if (!children[0].result) acc.push(choice);
},
// handler for verbatim text
word: async ({ result }, _, acc) => {
acc.push(result);
}
}
);
const __resolveVarName = (name, ctx) => {
if (name.indexOf(".") == -1) return name;
let resolved = "";
for (let x of name.split(".")) {
const $name = resolved + "." + x;
const $var = resolved ? ctx.vars[$name] : ctx.vars[x];
if ($var)
resolved = ($var.history.length ? peek($var.history) : "") || $name;
}
return resolved;
};
const __expandVar = async ({ children }, ctx) => {
const id = __resolveVarName(children[0].result, ctx);
if (id === "empty") return "";
const $var = ctx.vars[id];
if (!$var) {
if (ctx.missing !== void 0) {
return isString(ctx.missing) ? ctx.missing : ctx.missing(id);
}
throw new UnknownVariableError(id);
}
pickRandomUnique(1, $var.opts, $var.history, ctx.maxTrials, ctx.rnd);
if ($var.history.length > ctx.maxHist) $var.history.shift();
const choice = peek($var.history);
const result = await generate(choice, ctx);
if (result.err) {
throw result.err.message.includes("recursion") ? new CyclicReferenceError(id) : result.err;
}
let value = result.result;
if (children[1].children) {
for (let mod of children[1].children) {
const modFn = ctx.mods[mod.result];
if (modFn) value = await modFn(value);
else throw new UnknownModifierError(mod.result);
}
}
return value;
};
export {
CyclicReferenceError,
DEFAULT_MODIFIERS,
GRAMMAR,
UnknownModifierError,
UnknownVariableError,
generate
};