import {
AsHtml, AsText, BareWord, Concat, HtmlWord, InterpolatedPiece, Script, TextPiece, TextWord, Word
} from './words';
/**
* "Mode" of the environment a script runs in; determines access to mutability features and such.
*
* "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 = "action" | "render";
export type Proc = (
state: Vm,
argv: TextPiece[]
) => TextPiece;
/**
* 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
): TextWord | BareWord | HtmlWord {
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 {
return (
word.pieces
.map((piece) => evaluateWord(state, piece))
.reduce(Concat, null) ?? { text: "" }
);
}
}
/**
* 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) => void
): TextPiece {
let returnWord: TextPiece = { text: "" };
script.forEach((command) => {
const argv = command.map((word) => evaluateWord(state, word));
const name = AsText(argv[0]);
if (name in state.commands) {
returnWord = state.commands[name](state, argv);
} else {
// TODO: implement error propagation
returnWord = { html: `UNKNOWN COMMAND: ${AsHtml(argv[0])}` };
}
onReturn?.(returnWord);
});
return returnWord;
}