import { AsText, Concat, ErrorResult, InterpolatedPiece, ProcResult, Script, SourcePos, TextPiece, Word } from './words'; /** * "Mode" of the environment a script runs in; determines access to mutability features and such. * * "findingAction": preparing a response to a UI action, which can involve recalculating variables, but has no side-effects itself. * * "action": response to a UI action; allowed to modify card fields and access time and random numbers. * * "render": deterministic generation of display markup from card and workspace state; can only modify temporary variables. */ export type ScriptType = ["findingAction", SourcePos] | ["action"] | ["render"]; export type Proc = ( state: Vm, argv: TextPiece[] ) => ProcResult; /** * State for running a script in. */ export type Vm = { /** Mutability status */ mode: ScriptType; /** Implementations of commands scripts can run */ commands: Record>; /** Markup to render / output */ output: string; } & Context; function evaluateWord( state: Vm, word: Word | InterpolatedPiece ): TextPiece | ErrorResult { if ("bare" in word || "text" in word || "html" in word) { return word; } else if ("variable" in word) { return { text: "" }; } else if ("script" in word) { return runNoctl(state, word.script); } else { let fullWord = null; for (const piece of word.pieces) { const result = evaluateWord(state, piece); if ("error" in result) { return result; } else { fullWord = Concat(fullWord, result); } } return fullWord ?? { text: "" }; } } const NUMBER = /^\d+$/; /** * Runs a script in the context of a Noctl state. Potentially mutates the state. * * @param onReturn callback optionally invoked with the return word for each top-level command (not triggered by command substitutions) * @returns the return word of the final command in the script, or empty text if the script is empty. */ export function runNoctl( state: Vm, script: Script, onReturn?: (word: TextPiece | ErrorResult) => void ): TextPiece | ErrorResult { let returnWord: TextPiece | ErrorResult = { text: "" }; for (const command of script) { const argv: TextPiece[] = []; for (const word of command) { const processedWord = evaluateWord(state, word); if ("error" in processedWord) { onReturn?.(processedWord); return processedWord; } else { argv.push(processedWord); } } const name = AsText(argv[0]); if (name in state.commands) { returnWord = state.commands[name](state, argv); } else if (NUMBER.test(name) && "expr" in state.commands) { returnWord = state.commands.expr(state, argv); } else { returnWord = { error: `Unknown Command: ${name}` }; } onReturn?.(returnWord); } return returnWord; }