yarn-spinner-runner-ts
Version:
TypeScript parser, compiler, and runtime for Yarn Spinner 3.x with React adapter [NPM package](https://www.npmjs.com/package/yarn-spinner-runner-ts)
168 lines • 6.61 kB
JavaScript
/**
* Command parser and handler utilities for Yarn Spinner commands.
* Commands like <<command_name arg1 arg2>> or <<command_name "arg with spaces">>
*/
/**
* Parse a command string like "command_name arg1 arg2" or "set variable value"
*/
export function parseCommand(content) {
const trimmed = content.trim();
if (!trimmed) {
throw new Error("Empty command");
}
const parts = [];
let current = "";
let inQuotes = false;
let quoteChar = "";
for (let i = 0; i < trimmed.length; i++) {
const char = trimmed[i];
if ((char === '"' || char === "'") && !inQuotes) {
// If we have accumulated non-quoted content (e.g. a function name and "(")
// push it as its own part before entering quoted mode. This prevents the
// surrounding text from being merged into the quoted content when we
// later push the quoted value.
if (current.trim()) {
parts.push(current.trim());
current = "";
}
inQuotes = true;
quoteChar = char;
continue;
}
if (char === quoteChar && inQuotes) {
inQuotes = false;
// Preserve the surrounding quotes in the parsed part so callers that
// reassemble the expression (e.g. declare handlers) keep string literals
// intact instead of losing quote characters.
parts.push(quoteChar + current + quoteChar);
quoteChar = "";
current = "";
continue;
}
if (char === " " && !inQuotes) {
if (current.trim()) {
parts.push(current.trim());
current = "";
}
continue;
}
current += char;
}
if (current.trim()) {
parts.push(current.trim());
}
if (parts.length === 0) {
throw new Error("No command name found");
}
return {
name: parts[0],
args: parts.slice(1),
raw: content,
};
}
/**
* Built-in command handlers for common Yarn Spinner commands.
*/
export class CommandHandler {
constructor(variables = {}) {
this.handlers = new Map();
this.variables = variables;
this.registerBuiltins();
}
/**
* Register a command handler.
*/
register(name, handler) {
this.handlers.set(name.toLowerCase(), handler);
}
/**
* Execute a parsed command.
*/
async execute(parsed, evaluator) {
const handler = this.handlers.get(parsed.name.toLowerCase());
if (handler) {
await handler(parsed.args, evaluator);
}
else {
console.warn(`Unknown command: ${parsed.name}`);
}
}
registerBuiltins() {
// <<set $var to expr>> or <<set $var = expr>> or <<set $var expr>>
this.register("set", (args, evaluator) => {
if (!evaluator)
return;
if (args.length < 2)
return;
const varNameRaw = args[0];
let exprParts = args.slice(1);
if (exprParts[0] === "to")
exprParts = exprParts.slice(1);
if (exprParts[0] === "=")
exprParts = exprParts.slice(1);
const expr = exprParts.join(" ");
let value = evaluator.evaluateExpression(expr);
// If value is a string starting with ".", try to resolve as enum shorthand
if (typeof value === "string" && value.startsWith(".")) {
const enumType = evaluator.getEnumTypeForVariable(varNameRaw);
if (enumType) {
value = evaluator.resolveEnumValue(value, enumType);
}
}
const key = varNameRaw.startsWith("$") ? varNameRaw.slice(1) : varNameRaw;
// Setting a variable converts it from smart to regular
this.variables[key] = value;
evaluator.setVariable(key, value);
});
// <<declare $var = expr>>
this.register("declare", (args, evaluator) => {
if (!evaluator)
return;
if (args.length < 3)
return; // name, '=', expr
const varNameRaw = args[0];
let exprParts = args.slice(1);
if (exprParts[0] === "=")
exprParts = exprParts.slice(1);
const expr = exprParts.join(" ");
const key = varNameRaw.startsWith("$") ? varNameRaw.slice(1) : varNameRaw;
// Check if expression is "smart" (contains operators, comparisons, or variable references)
// Smart variables: expressions with operators, comparisons, logical ops, or function calls
const isSmart = /[+\-*/%<>=!&|]/.test(expr) ||
/\$\w+/.test(expr) || // references other variables
/[a-zA-Z_]\w*\s*\(/.test(expr); // function calls
if (isSmart) {
// Store as smart variable - will recalculate on each access
evaluator.setSmartVariable(key, expr);
// Also store initial value in variables for immediate use
const initialValue = evaluator.evaluateExpression(expr);
this.variables[key] = initialValue;
}
else {
// Regular variable - evaluate once and store
let value = evaluator.evaluateExpression(expr);
// Check if expr is an enum value (EnumName.CaseName or .CaseName)
if (typeof value === "string") {
// Try to extract enum name from EnumName.CaseName
const enumMatch = expr.match(/^([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)$/);
if (enumMatch) {
const enumName = enumMatch[1];
value = evaluator.resolveEnumValue(expr, enumName);
}
else if (value.startsWith(".")) {
// Shorthand - we can't infer enum type from declaration alone
// Store as-is, will be resolved on first use if variable has enum type
// Value is already set correctly above
}
}
this.variables[key] = value;
evaluator.setVariable(key, value);
}
});
// <<stop>> - no-op, just a marker
this.register("stop", () => {
// Dialogue stop marker
});
}
}
//# sourceMappingURL=commands.js.map