yarn-spinner-runner-ts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -0
- package/dist/compile/compiler.d.ts +9 -0
- package/dist/compile/compiler.js +172 -0
- package/dist/compile/compiler.js.map +1 -0
- package/dist/compile/ir.d.ts +47 -0
- package/dist/compile/ir.js +2 -0
- package/dist/compile/ir.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/model/ast.d.ts +72 -0
- package/dist/model/ast.js +2 -0
- package/dist/model/ast.js.map +1 -0
- package/dist/parse/lexer.d.ts +7 -0
- package/dist/parse/lexer.js +78 -0
- package/dist/parse/lexer.js.map +1 -0
- package/dist/parse/parser.d.ts +4 -0
- package/dist/parse/parser.js +433 -0
- package/dist/parse/parser.js.map +1 -0
- package/dist/runtime/commands.d.ts +31 -0
- package/dist/runtime/commands.js +157 -0
- package/dist/runtime/commands.js.map +1 -0
- package/dist/runtime/evaluator.d.ts +52 -0
- package/dist/runtime/evaluator.js +309 -0
- package/dist/runtime/evaluator.js.map +1 -0
- package/dist/runtime/results.d.ts +22 -0
- package/dist/runtime/results.js +2 -0
- package/dist/runtime/results.js.map +1 -0
- package/dist/runtime/runner.d.ts +63 -0
- package/dist/runtime/runner.js +456 -0
- package/dist/runtime/runner.js.map +1 -0
- package/dist/tests/full_featured.test.d.ts +1 -0
- package/dist/tests/full_featured.test.js +130 -0
- package/dist/tests/full_featured.test.js.map +1 -0
- package/dist/tests/index.test.d.ts +1 -0
- package/dist/tests/index.test.js +30 -0
- package/dist/tests/index.test.js.map +1 -0
- package/dist/tests/jump_detour.test.d.ts +1 -0
- package/dist/tests/jump_detour.test.js +47 -0
- package/dist/tests/jump_detour.test.js.map +1 -0
- package/dist/tests/nodes_lines.test.d.ts +1 -0
- package/dist/tests/nodes_lines.test.js +23 -0
- package/dist/tests/nodes_lines.test.js.map +1 -0
- package/dist/tests/once.test.d.ts +1 -0
- package/dist/tests/once.test.js +29 -0
- package/dist/tests/once.test.js.map +1 -0
- package/dist/tests/options.test.d.ts +1 -0
- package/dist/tests/options.test.js +32 -0
- package/dist/tests/options.test.js.map +1 -0
- package/dist/tests/variables_flow_cmds.test.d.ts +1 -0
- package/dist/tests/variables_flow_cmds.test.js +30 -0
- package/dist/tests/variables_flow_cmds.test.js.map +1 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/docs/commands.md +21 -0
- package/docs/compatibility-checklist.md +77 -0
- package/docs/css-attribute.md +47 -0
- package/docs/detour.md +24 -0
- package/docs/enums.md +25 -0
- package/docs/flow-control.md +25 -0
- package/docs/functions.md +20 -0
- package/docs/jumps.md +24 -0
- package/docs/line-groups.md +21 -0
- package/docs/lines-nodes-and-options.md +30 -0
- package/docs/logic-and-variables.md +25 -0
- package/docs/markup.md +19 -0
- package/docs/node-groups.md +13 -0
- package/docs/once.md +21 -0
- package/docs/options.md +45 -0
- package/docs/saliency.md +25 -0
- package/docs/scenes-actors-setup.md +195 -0
- package/docs/scenes.md +64 -0
- package/docs/shadow-lines.md +18 -0
- package/docs/smart-variables.md +19 -0
- package/docs/storylets-and-saliency-a-primer.md +14 -0
- package/docs/tags-metadata.md +18 -0
- package/eslint.config.cjs +33 -0
- package/examples/browser/README.md +40 -0
- package/examples/browser/index.html +23 -0
- package/examples/browser/main.tsx +16 -0
- package/examples/browser/vite.config.ts +22 -0
- package/examples/react/DialogueExample.tsx +2 -0
- package/examples/react/DialogueView.tsx +2 -0
- package/examples/react/useYarnRunner.tsx +2 -0
- package/examples/scenes/scenes.yaml +10 -0
- package/examples/yarn/full_featured.yarn +43 -0
- package/package.json +55 -0
- package/src/compile/compiler.ts +183 -0
- package/src/compile/ir.ts +28 -0
- package/src/index.ts +17 -0
- package/src/model/ast.ts +93 -0
- package/src/parse/lexer.ts +108 -0
- package/src/parse/parser.ts +435 -0
- package/src/react/DialogueExample.tsx +149 -0
- package/src/react/DialogueScene.tsx +107 -0
- package/src/react/DialogueView.tsx +160 -0
- package/src/react/dialogue.css +181 -0
- package/src/react/useYarnRunner.tsx +33 -0
- package/src/runtime/commands.ts +183 -0
- package/src/runtime/evaluator.ts +327 -0
- package/src/runtime/results.ts +27 -0
- package/src/runtime/runner.ts +480 -0
- package/src/scene/parser.ts +83 -0
- package/src/scene/types.ts +17 -0
- package/src/tests/full_featured.test.ts +131 -0
- package/src/tests/index.test.ts +34 -0
- package/src/tests/jump_detour.test.ts +47 -0
- package/src/tests/nodes_lines.test.ts +27 -0
- package/src/tests/once.test.ts +32 -0
- package/src/tests/options.test.ts +34 -0
- package/src/tests/variables_flow_cmds.test.ts +33 -0
- package/src/types.ts +4 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe expression evaluator for Yarn Spinner conditions.
|
|
3
|
+
* Supports variables, functions, comparisons, and logical operators.
|
|
4
|
+
*/
|
|
5
|
+
export class ExpressionEvaluator {
|
|
6
|
+
private smartVariables: Record<string, string> = {}; // variable name -> expression
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
private variables: Record<string, unknown> = {},
|
|
10
|
+
private functions: Record<string, (...args: unknown[]) => unknown> = {},
|
|
11
|
+
private enums: Record<string, string[]> = {} // enum name -> cases
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Evaluate a condition expression and return a boolean result.
|
|
16
|
+
* Supports: variables, literals (numbers, strings, booleans), comparisons, logical ops, function calls.
|
|
17
|
+
*/
|
|
18
|
+
evaluate(expr: string): boolean {
|
|
19
|
+
try {
|
|
20
|
+
const result = this.evaluateExpression(expr);
|
|
21
|
+
return !!result;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Evaluate an expression that can return any value (not just boolean).
|
|
29
|
+
*/
|
|
30
|
+
evaluateExpression(expr: string): unknown {
|
|
31
|
+
const trimmed = this.preprocess(expr.trim());
|
|
32
|
+
if (!trimmed) return false;
|
|
33
|
+
|
|
34
|
+
// Handle function calls like `functionName(arg1, arg2)`
|
|
35
|
+
if (trimmed.includes("(") && trimmed.includes(")")) {
|
|
36
|
+
return this.evaluateFunctionCall(trimmed);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Handle comparisons
|
|
40
|
+
if (this.containsComparison(trimmed)) {
|
|
41
|
+
return this.evaluateComparison(trimmed);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Handle logical operators
|
|
45
|
+
if (trimmed.includes("&&") || trimmed.includes("||")) {
|
|
46
|
+
return this.evaluateLogical(trimmed);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Handle negation
|
|
50
|
+
if (trimmed.startsWith("!")) {
|
|
51
|
+
return !this.evaluateExpression(trimmed.slice(1).trim());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Simple variable or literal
|
|
55
|
+
return this.resolveValue(trimmed);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private preprocess(expr: string): string {
|
|
59
|
+
// Normalize operator word aliases to JS-like symbols
|
|
60
|
+
// Whole word replacements only
|
|
61
|
+
return expr
|
|
62
|
+
.replace(/\bnot\b/gi, "!")
|
|
63
|
+
.replace(/\band\b/gi, "&&")
|
|
64
|
+
.replace(/\bor\b/gi, "||")
|
|
65
|
+
.replace(/\bxor\b/gi, "^")
|
|
66
|
+
.replace(/\beq\b|\bis\b/gi, "==")
|
|
67
|
+
.replace(/\bneq\b/gi, "!=")
|
|
68
|
+
.replace(/\bgte\b/gi, ">=")
|
|
69
|
+
.replace(/\blte\b/gi, "<=")
|
|
70
|
+
.replace(/\bgt\b/gi, ">")
|
|
71
|
+
.replace(/\blt\b/gi, "<");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private evaluateFunctionCall(expr: string): unknown {
|
|
75
|
+
const match = expr.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*\((.*)\)$/);
|
|
76
|
+
if (!match) throw new Error(`Invalid function call: ${expr}`);
|
|
77
|
+
|
|
78
|
+
const [, name, argsStr] = match;
|
|
79
|
+
const func = this.functions[name];
|
|
80
|
+
if (!func) throw new Error(`Function not found: ${name}`);
|
|
81
|
+
|
|
82
|
+
const args = this.parseArguments(argsStr);
|
|
83
|
+
const evaluatedArgs = args.map((arg) => this.evaluateExpression(arg.trim()));
|
|
84
|
+
|
|
85
|
+
return func(...evaluatedArgs);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private parseArguments(argsStr: string): string[] {
|
|
89
|
+
if (!argsStr.trim()) return [];
|
|
90
|
+
const args: string[] = [];
|
|
91
|
+
let depth = 0;
|
|
92
|
+
let current = "";
|
|
93
|
+
for (const char of argsStr) {
|
|
94
|
+
if (char === "(") depth++;
|
|
95
|
+
else if (char === ")") depth--;
|
|
96
|
+
else if (char === "," && depth === 0) {
|
|
97
|
+
args.push(current.trim());
|
|
98
|
+
current = "";
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
current += char;
|
|
102
|
+
}
|
|
103
|
+
if (current.trim()) args.push(current.trim());
|
|
104
|
+
return args;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private containsComparison(expr: string): boolean {
|
|
108
|
+
return /[<>=!]/.test(expr);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private evaluateComparison(expr: string): boolean {
|
|
112
|
+
// Match comparison operators (avoid matching !=, <=, >=)
|
|
113
|
+
const match = expr.match(/^(.+?)\s*(===|!==|<=|>=|<|>)\s*(.+)$/);
|
|
114
|
+
if (!match) throw new Error(`Invalid comparison: ${expr}`);
|
|
115
|
+
|
|
116
|
+
const [, left, op, right] = match;
|
|
117
|
+
const leftVal = this.evaluateExpression(left.trim());
|
|
118
|
+
const rightVal = this.evaluateExpression(right.trim());
|
|
119
|
+
|
|
120
|
+
switch (op) {
|
|
121
|
+
case "===":
|
|
122
|
+
case "==":
|
|
123
|
+
return this.deepEquals(leftVal, rightVal);
|
|
124
|
+
case "!==":
|
|
125
|
+
case "!=":
|
|
126
|
+
return !this.deepEquals(leftVal, rightVal);
|
|
127
|
+
case "<":
|
|
128
|
+
return Number(leftVal) < Number(rightVal);
|
|
129
|
+
case ">":
|
|
130
|
+
return Number(leftVal) > Number(rightVal);
|
|
131
|
+
case "<=":
|
|
132
|
+
return Number(leftVal) <= Number(rightVal);
|
|
133
|
+
case ">=":
|
|
134
|
+
return Number(leftVal) >= Number(rightVal);
|
|
135
|
+
default:
|
|
136
|
+
throw new Error(`Unknown operator: ${op}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private evaluateLogical(expr: string): boolean {
|
|
141
|
+
// Split by && or ||, respecting parentheses
|
|
142
|
+
const parts: Array<{ expr: string; op: "&&" | "||" | null }> = [];
|
|
143
|
+
let depth = 0;
|
|
144
|
+
let current = "";
|
|
145
|
+
let lastOp: "&&" | "||" | null = null;
|
|
146
|
+
|
|
147
|
+
for (const char of expr) {
|
|
148
|
+
if (char === "(") depth++;
|
|
149
|
+
else if (char === ")") depth--;
|
|
150
|
+
else if (depth === 0 && expr.includes(char === "&" ? "&&" : char === "|" ? "||" : "")) {
|
|
151
|
+
// Check for && or ||
|
|
152
|
+
const remaining = expr.slice(expr.indexOf(char));
|
|
153
|
+
if (remaining.startsWith("&&")) {
|
|
154
|
+
if (current.trim()) {
|
|
155
|
+
parts.push({ expr: current.trim(), op: lastOp });
|
|
156
|
+
current = "";
|
|
157
|
+
}
|
|
158
|
+
lastOp = "&&";
|
|
159
|
+
// skip &&
|
|
160
|
+
continue;
|
|
161
|
+
} else if (remaining.startsWith("||")) {
|
|
162
|
+
if (current.trim()) {
|
|
163
|
+
parts.push({ expr: current.trim(), op: lastOp });
|
|
164
|
+
current = "";
|
|
165
|
+
}
|
|
166
|
+
lastOp = "||";
|
|
167
|
+
// skip ||
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
current += char;
|
|
172
|
+
}
|
|
173
|
+
if (current.trim()) parts.push({ expr: current.trim(), op: lastOp });
|
|
174
|
+
|
|
175
|
+
// Simple case: single expression
|
|
176
|
+
if (parts.length === 0) return !!this.evaluateExpression(expr);
|
|
177
|
+
|
|
178
|
+
// Evaluate parts (supports &&, ||, ^ as xor)
|
|
179
|
+
let result = this.evaluateExpression(parts[0].expr);
|
|
180
|
+
for (let i = 1; i < parts.length; i++) {
|
|
181
|
+
const part = parts[i];
|
|
182
|
+
const val = this.evaluateExpression(part.expr);
|
|
183
|
+
if (part.op === "&&") {
|
|
184
|
+
result = result && val;
|
|
185
|
+
} else if (part.op === "||") {
|
|
186
|
+
result = result || val;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return !!result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private resolveValue(expr: string): unknown {
|
|
194
|
+
// Try enum syntax: EnumName.CaseName or .CaseName
|
|
195
|
+
const enumMatch = expr.match(/^\.?([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
196
|
+
if (enumMatch) {
|
|
197
|
+
const [, enumName, caseName] = enumMatch;
|
|
198
|
+
if (this.enums[enumName] && this.enums[enumName].includes(caseName)) {
|
|
199
|
+
return `${enumName}.${caseName}`; // Store as "EnumName.CaseName" string
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Try shorthand enum: .CaseName (requires context from variables)
|
|
204
|
+
if (expr.startsWith(".") && expr.length > 1) {
|
|
205
|
+
const caseName = expr.slice(1);
|
|
206
|
+
// Try to infer enum from variable types - for now, return as-is and let validation handle it
|
|
207
|
+
return expr;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Try as variable first
|
|
211
|
+
const key = expr.startsWith("$") ? expr.slice(1) : expr;
|
|
212
|
+
|
|
213
|
+
// Check if this is a smart variable (has stored expression)
|
|
214
|
+
if (Object.prototype.hasOwnProperty.call(this.smartVariables, key)) {
|
|
215
|
+
// Re-evaluate the expression each time it's accessed
|
|
216
|
+
return this.evaluateExpression(this.smartVariables[key]);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (Object.prototype.hasOwnProperty.call(this.variables, key)) {
|
|
220
|
+
return this.variables[key];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Try as number
|
|
224
|
+
const num = Number(expr);
|
|
225
|
+
if (!isNaN(num) && expr.trim() === String(num)) {
|
|
226
|
+
return num;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Try as boolean
|
|
230
|
+
if (expr === "true") return true;
|
|
231
|
+
if (expr === "false") return false;
|
|
232
|
+
|
|
233
|
+
// Try as string (quoted)
|
|
234
|
+
if ((expr.startsWith('"') && expr.endsWith('"')) || (expr.startsWith("'") && expr.endsWith("'"))) {
|
|
235
|
+
return expr.slice(1, -1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Default: treat as variable (may be undefined)
|
|
239
|
+
return this.variables[key];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Resolve shorthand enum (.CaseName) when setting a variable with known enum type
|
|
244
|
+
*/
|
|
245
|
+
resolveEnumValue(expr: string, enumName?: string): string {
|
|
246
|
+
if (expr.startsWith(".") && enumName) {
|
|
247
|
+
const caseName = expr.slice(1);
|
|
248
|
+
if (this.enums[enumName] && this.enums[enumName].includes(caseName)) {
|
|
249
|
+
return `${enumName}.${caseName}`;
|
|
250
|
+
}
|
|
251
|
+
throw new Error(`Invalid enum case ${caseName} for enum ${enumName}`);
|
|
252
|
+
}
|
|
253
|
+
// Check if it's already EnumName.CaseName format
|
|
254
|
+
const match = expr.match(/^([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
255
|
+
if (match) {
|
|
256
|
+
const [, name, caseName] = match;
|
|
257
|
+
if (this.enums[name] && this.enums[name].includes(caseName)) {
|
|
258
|
+
return expr;
|
|
259
|
+
}
|
|
260
|
+
throw new Error(`Invalid enum case ${caseName} for enum ${name}`);
|
|
261
|
+
}
|
|
262
|
+
return expr;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get enum type for a variable (if it was declared with enum type)
|
|
267
|
+
*/
|
|
268
|
+
getEnumTypeForVariable(varName: string): string | undefined {
|
|
269
|
+
// Check if variable value matches EnumName.CaseName pattern
|
|
270
|
+
const key = varName.startsWith("$") ? varName.slice(1) : varName;
|
|
271
|
+
const value = this.variables[key];
|
|
272
|
+
if (typeof value === "string") {
|
|
273
|
+
const match = value.match(/^([A-Za-z_][A-Za-z0-9_]*)\./);
|
|
274
|
+
if (match) {
|
|
275
|
+
return match[1];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private deepEquals(a: unknown, b: unknown): boolean {
|
|
282
|
+
if (a === b) return true;
|
|
283
|
+
if (a == null || b == null) return a === b;
|
|
284
|
+
if (typeof a !== typeof b) return false;
|
|
285
|
+
if (typeof a === "object") {
|
|
286
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
287
|
+
}
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Update variables. Can be used to mutate state during dialogue.
|
|
293
|
+
*/
|
|
294
|
+
setVariable(name: string, value: unknown): void {
|
|
295
|
+
// If setting a smart variable, remove it (converting to regular variable)
|
|
296
|
+
if (Object.prototype.hasOwnProperty.call(this.smartVariables, name)) {
|
|
297
|
+
delete this.smartVariables[name];
|
|
298
|
+
}
|
|
299
|
+
this.variables[name] = value;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Register a smart variable (variable with expression that recalculates on access).
|
|
304
|
+
*/
|
|
305
|
+
setSmartVariable(name: string, expression: string): void {
|
|
306
|
+
// Remove from regular variables if it exists
|
|
307
|
+
if (Object.prototype.hasOwnProperty.call(this.variables, name)) {
|
|
308
|
+
delete this.variables[name];
|
|
309
|
+
}
|
|
310
|
+
this.smartVariables[name] = expression;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Check if a variable is a smart variable.
|
|
315
|
+
*/
|
|
316
|
+
isSmartVariable(name: string): boolean {
|
|
317
|
+
return Object.prototype.hasOwnProperty.call(this.smartVariables, name);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get variable value.
|
|
322
|
+
*/
|
|
323
|
+
getVariable(name: string): unknown {
|
|
324
|
+
return this.variables[name];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type TextResult = {
|
|
2
|
+
type: "text";
|
|
3
|
+
text: string;
|
|
4
|
+
speaker?: string;
|
|
5
|
+
tags?: string[];
|
|
6
|
+
nodeCss?: string; // Node-level CSS from &css{} header
|
|
7
|
+
scene?: string; // Scene name from node header
|
|
8
|
+
isDialogueEnd: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type OptionsResult = {
|
|
12
|
+
type: "options";
|
|
13
|
+
options: { text: string; tags?: string[]; css?: string }[];
|
|
14
|
+
nodeCss?: string; // Node-level CSS from &css{} header
|
|
15
|
+
scene?: string; // Scene name from node header
|
|
16
|
+
isDialogueEnd: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type CommandResult = {
|
|
20
|
+
type: "command";
|
|
21
|
+
command: string;
|
|
22
|
+
isDialogueEnd: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type RuntimeResult = TextResult | OptionsResult | CommandResult;
|
|
26
|
+
|
|
27
|
+
|