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.
Files changed (114) hide show
  1. package/README.md +180 -0
  2. package/dist/compile/compiler.d.ts +9 -0
  3. package/dist/compile/compiler.js +172 -0
  4. package/dist/compile/compiler.js.map +1 -0
  5. package/dist/compile/ir.d.ts +47 -0
  6. package/dist/compile/ir.js +2 -0
  7. package/dist/compile/ir.js.map +1 -0
  8. package/dist/index.d.ts +10 -0
  9. package/dist/index.js +11 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/model/ast.d.ts +72 -0
  12. package/dist/model/ast.js +2 -0
  13. package/dist/model/ast.js.map +1 -0
  14. package/dist/parse/lexer.d.ts +7 -0
  15. package/dist/parse/lexer.js +78 -0
  16. package/dist/parse/lexer.js.map +1 -0
  17. package/dist/parse/parser.d.ts +4 -0
  18. package/dist/parse/parser.js +433 -0
  19. package/dist/parse/parser.js.map +1 -0
  20. package/dist/runtime/commands.d.ts +31 -0
  21. package/dist/runtime/commands.js +157 -0
  22. package/dist/runtime/commands.js.map +1 -0
  23. package/dist/runtime/evaluator.d.ts +52 -0
  24. package/dist/runtime/evaluator.js +309 -0
  25. package/dist/runtime/evaluator.js.map +1 -0
  26. package/dist/runtime/results.d.ts +22 -0
  27. package/dist/runtime/results.js +2 -0
  28. package/dist/runtime/results.js.map +1 -0
  29. package/dist/runtime/runner.d.ts +63 -0
  30. package/dist/runtime/runner.js +456 -0
  31. package/dist/runtime/runner.js.map +1 -0
  32. package/dist/tests/full_featured.test.d.ts +1 -0
  33. package/dist/tests/full_featured.test.js +130 -0
  34. package/dist/tests/full_featured.test.js.map +1 -0
  35. package/dist/tests/index.test.d.ts +1 -0
  36. package/dist/tests/index.test.js +30 -0
  37. package/dist/tests/index.test.js.map +1 -0
  38. package/dist/tests/jump_detour.test.d.ts +1 -0
  39. package/dist/tests/jump_detour.test.js +47 -0
  40. package/dist/tests/jump_detour.test.js.map +1 -0
  41. package/dist/tests/nodes_lines.test.d.ts +1 -0
  42. package/dist/tests/nodes_lines.test.js +23 -0
  43. package/dist/tests/nodes_lines.test.js.map +1 -0
  44. package/dist/tests/once.test.d.ts +1 -0
  45. package/dist/tests/once.test.js +29 -0
  46. package/dist/tests/once.test.js.map +1 -0
  47. package/dist/tests/options.test.d.ts +1 -0
  48. package/dist/tests/options.test.js +32 -0
  49. package/dist/tests/options.test.js.map +1 -0
  50. package/dist/tests/variables_flow_cmds.test.d.ts +1 -0
  51. package/dist/tests/variables_flow_cmds.test.js +30 -0
  52. package/dist/tests/variables_flow_cmds.test.js.map +1 -0
  53. package/dist/types.d.ts +3 -0
  54. package/dist/types.js +2 -0
  55. package/dist/types.js.map +1 -0
  56. package/docs/commands.md +21 -0
  57. package/docs/compatibility-checklist.md +77 -0
  58. package/docs/css-attribute.md +47 -0
  59. package/docs/detour.md +24 -0
  60. package/docs/enums.md +25 -0
  61. package/docs/flow-control.md +25 -0
  62. package/docs/functions.md +20 -0
  63. package/docs/jumps.md +24 -0
  64. package/docs/line-groups.md +21 -0
  65. package/docs/lines-nodes-and-options.md +30 -0
  66. package/docs/logic-and-variables.md +25 -0
  67. package/docs/markup.md +19 -0
  68. package/docs/node-groups.md +13 -0
  69. package/docs/once.md +21 -0
  70. package/docs/options.md +45 -0
  71. package/docs/saliency.md +25 -0
  72. package/docs/scenes-actors-setup.md +195 -0
  73. package/docs/scenes.md +64 -0
  74. package/docs/shadow-lines.md +18 -0
  75. package/docs/smart-variables.md +19 -0
  76. package/docs/storylets-and-saliency-a-primer.md +14 -0
  77. package/docs/tags-metadata.md +18 -0
  78. package/eslint.config.cjs +33 -0
  79. package/examples/browser/README.md +40 -0
  80. package/examples/browser/index.html +23 -0
  81. package/examples/browser/main.tsx +16 -0
  82. package/examples/browser/vite.config.ts +22 -0
  83. package/examples/react/DialogueExample.tsx +2 -0
  84. package/examples/react/DialogueView.tsx +2 -0
  85. package/examples/react/useYarnRunner.tsx +2 -0
  86. package/examples/scenes/scenes.yaml +10 -0
  87. package/examples/yarn/full_featured.yarn +43 -0
  88. package/package.json +55 -0
  89. package/src/compile/compiler.ts +183 -0
  90. package/src/compile/ir.ts +28 -0
  91. package/src/index.ts +17 -0
  92. package/src/model/ast.ts +93 -0
  93. package/src/parse/lexer.ts +108 -0
  94. package/src/parse/parser.ts +435 -0
  95. package/src/react/DialogueExample.tsx +149 -0
  96. package/src/react/DialogueScene.tsx +107 -0
  97. package/src/react/DialogueView.tsx +160 -0
  98. package/src/react/dialogue.css +181 -0
  99. package/src/react/useYarnRunner.tsx +33 -0
  100. package/src/runtime/commands.ts +183 -0
  101. package/src/runtime/evaluator.ts +327 -0
  102. package/src/runtime/results.ts +27 -0
  103. package/src/runtime/runner.ts +480 -0
  104. package/src/scene/parser.ts +83 -0
  105. package/src/scene/types.ts +17 -0
  106. package/src/tests/full_featured.test.ts +131 -0
  107. package/src/tests/index.test.ts +34 -0
  108. package/src/tests/jump_detour.test.ts +47 -0
  109. package/src/tests/nodes_lines.test.ts +27 -0
  110. package/src/tests/once.test.ts +32 -0
  111. package/src/tests/options.test.ts +34 -0
  112. package/src/tests/variables_flow_cmds.test.ts +33 -0
  113. package/src/types.ts +4 -0
  114. 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
+