UNPKG

@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
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 };