75 lines
2.2 KiB
TypeScript
75 lines
2.2 KiB
TypeScript
|
import {
|
||
|
AsHtml, AsText, Concat, EnchantedWord, 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<string, Proc>;
|
||
|
/** Markup to render / output */
|
||
|
output: string;
|
||
|
};
|
||
|
|
||
|
function evaluateWord(
|
||
|
state: Vm,
|
||
|
word: Word | InterpolatedPiece
|
||
|
): TextWord | EnchantedWord | HtmlWord {
|
||
|
if ("enchanted" 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: `<b>UNKNOWN COMMAND: ${AsHtml(argv[0])}</b>` };
|
||
|
}
|
||
|
onReturn?.(returnWord);
|
||
|
});
|
||
|
|
||
|
return returnWord;
|
||
|
}
|