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,183 @@
1
+ import type { YarnDocument, Statement, Line, Option } from "../model/ast";
2
+ import type { IRProgram, IRNode, IRNodeGroup, IRInstruction } from "./ir";
3
+
4
+ export interface CompileOptions {
5
+ generateOnceIds?: (ctx: { node: string; index: number }) => string;
6
+ }
7
+
8
+ export function compile(doc: YarnDocument, opts: CompileOptions = {}): IRProgram {
9
+ const program: IRProgram = { enums: {}, nodes: {} };
10
+ // Store enum definitions
11
+ for (const enumDef of doc.enums) {
12
+ program.enums[enumDef.name] = enumDef.cases;
13
+ }
14
+ const genOnce = opts.generateOnceIds ?? ((x) => `${x.node}#once#${x.index}`);
15
+ let globalLineCounter = 0;
16
+
17
+ function ensureLineId(tags?: string[]): string[] | undefined {
18
+ const t = tags ? [...tags] : [];
19
+ if (!t.some((x) => x.startsWith("line:"))) {
20
+ t.push(`line:${(globalLineCounter++).toString(16)}`);
21
+ }
22
+ return t;
23
+ }
24
+
25
+ // Group nodes by title to handle node groups
26
+ const nodesByTitle = new Map<string, typeof doc.nodes>();
27
+ for (const node of doc.nodes) {
28
+ if (!nodesByTitle.has(node.title)) {
29
+ nodesByTitle.set(node.title, []);
30
+ }
31
+ nodesByTitle.get(node.title)!.push(node);
32
+ }
33
+
34
+ for (const [title, nodesWithSameTitle] of nodesByTitle) {
35
+ // If only one node with this title, treat as regular node
36
+ if (nodesWithSameTitle.length === 1) {
37
+ const node = nodesWithSameTitle[0];
38
+ const instructions: IRInstruction[] = [];
39
+ let onceCounter = 0;
40
+ function emitBlock(stmts: Statement[]): IRInstruction[] {
41
+ const block: IRInstruction[] = [];
42
+ for (const s of stmts) {
43
+ switch (s.type) {
44
+ case "Line":
45
+ {
46
+ const line = s as Line;
47
+ block.push({ op: "line", speaker: line.speaker, text: line.text, tags: ensureLineId(line.tags) });
48
+ }
49
+ break;
50
+ case "Command":
51
+ block.push({ op: "command", content: s.content });
52
+ break;
53
+ case "Jump":
54
+ block.push({ op: "jump", target: s.target });
55
+ break;
56
+ case "Detour":
57
+ block.push({ op: "detour", target: s.target });
58
+ break;
59
+ case "OptionGroup": {
60
+ // Add #lastline tag to the most recent line, if present
61
+ for (let i = block.length - 1; i >= 0; i--) {
62
+ const ins = block[i];
63
+ if (ins.op === "line") {
64
+ const tags = new Set(ins.tags ?? []);
65
+ if (![...tags].some((x) => x === "lastline" || x === "#lastline")) {
66
+ tags.add("lastline");
67
+ }
68
+ ins.tags = Array.from(tags);
69
+ break;
70
+ }
71
+ if (ins.op !== "command") break; // stop if non-line non-command before options
72
+ }
73
+ block.push({
74
+ op: "options",
75
+ options: s.options.map((o: Option) => ({ text: o.text, tags: ensureLineId(o.tags), css: (o as any).css, block: emitBlock(o.body) })),
76
+ });
77
+ break;
78
+ }
79
+ case "If":
80
+ block.push({
81
+ op: "if",
82
+ branches: s.branches.map((b) => ({ condition: b.condition, block: emitBlock(b.body) })),
83
+ });
84
+ break;
85
+ case "Once":
86
+ block.push({ op: "once", id: genOnce({ node: node.title, index: onceCounter++ }), block: emitBlock(s.body) });
87
+ break;
88
+ case "Enum":
89
+ // Enums are metadata, skip during compilation (already stored in program.enums)
90
+ break;
91
+ }
92
+ }
93
+ return block;
94
+ }
95
+ instructions.push(...emitBlock(node.body));
96
+ const irNode: IRNode = {
97
+ title: node.title,
98
+ instructions,
99
+ when: node.when,
100
+ css: (node as any).css,
101
+ scene: node.headers.scene?.trim() || undefined
102
+ };
103
+ program.nodes[node.title] = irNode;
104
+ } else {
105
+ // Multiple nodes with same title - create node group
106
+ const groupNodes: IRNode[] = [];
107
+ for (const node of nodesWithSameTitle) {
108
+ const instructions: IRInstruction[] = [];
109
+ let onceCounter = 0;
110
+ function emitBlock(stmts: Statement[]): IRInstruction[] {
111
+ const block: IRInstruction[] = [];
112
+ for (const s of stmts) {
113
+ switch (s.type) {
114
+ case "Line":
115
+ {
116
+ const line = s as Line;
117
+ block.push({ op: "line", speaker: line.speaker, text: line.text, tags: ensureLineId(line.tags) });
118
+ }
119
+ break;
120
+ case "Command":
121
+ block.push({ op: "command", content: s.content });
122
+ break;
123
+ case "Jump":
124
+ block.push({ op: "jump", target: s.target });
125
+ break;
126
+ case "Detour":
127
+ block.push({ op: "detour", target: s.target });
128
+ break;
129
+ case "OptionGroup": {
130
+ for (let i = block.length - 1; i >= 0; i--) {
131
+ const ins = block[i];
132
+ if (ins.op === "line") {
133
+ const tags = new Set(ins.tags ?? []);
134
+ if (![...tags].some((x) => x === "lastline" || x === "#lastline")) {
135
+ tags.add("lastline");
136
+ }
137
+ ins.tags = Array.from(tags);
138
+ break;
139
+ }
140
+ if (ins.op !== "command") break;
141
+ }
142
+ block.push({
143
+ op: "options",
144
+ options: s.options.map((o: Option) => ({ text: o.text, tags: ensureLineId(o.tags), css: (o as any).css, block: emitBlock(o.body) })),
145
+ });
146
+ break;
147
+ }
148
+ case "If":
149
+ block.push({
150
+ op: "if",
151
+ branches: s.branches.map((b) => ({ condition: b.condition, block: emitBlock(b.body) })),
152
+ });
153
+ break;
154
+ case "Once":
155
+ block.push({ op: "once", id: genOnce({ node: node.title, index: onceCounter++ }), block: emitBlock(s.body) });
156
+ break;
157
+ case "Enum":
158
+ break;
159
+ }
160
+ }
161
+ return block;
162
+ }
163
+ instructions.push(...emitBlock(node.body));
164
+ groupNodes.push({
165
+ title: node.title,
166
+ instructions,
167
+ when: node.when,
168
+ css: (node as any).css,
169
+ scene: node.headers.scene?.trim() || undefined
170
+ });
171
+ }
172
+ const group: IRNodeGroup = {
173
+ title,
174
+ nodes: groupNodes
175
+ };
176
+ program.nodes[title] = group;
177
+ }
178
+ }
179
+
180
+ return program;
181
+ }
182
+
183
+
@@ -0,0 +1,28 @@
1
+ export type IRProgram = {
2
+ enums: Record<string, string[]>; // enum name -> cases
3
+ nodes: Record<string, IRNode | IRNodeGroup>; // can be single node or group
4
+ };
5
+
6
+ export type IRNode = {
7
+ title: string;
8
+ instructions: IRInstruction[];
9
+ when?: string[]; // Array of when conditions
10
+ css?: string;
11
+ scene?: string; // Scene name from node header
12
+ };
13
+
14
+ export type IRNodeGroup = {
15
+ title: string;
16
+ nodes: IRNode[]; // Multiple nodes with same title, different when conditions
17
+ };
18
+
19
+ export type IRInstruction =
20
+ | { op: "line"; speaker?: string; text: string; tags?: string[] }
21
+ | { op: "command"; content: string }
22
+ | { op: "jump"; target: string }
23
+ | { op: "detour"; target: string }
24
+ | { op: "options"; options: Array<{ text: string; tags?: string[]; css?: string; block: IRInstruction[] }> }
25
+ | { op: "if"; branches: Array<{ condition: string | null; block: IRInstruction[] }> }
26
+ | { op: "once"; id: string; block: IRInstruction[] };
27
+
28
+
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export * from "./model/ast.js";
2
+ export * from "./parse/lexer.js";
3
+ export * from "./parse/parser.js";
4
+ export * from "./compile/ir.js";
5
+ export * from "./compile/compiler.js";
6
+ export * from "./runtime/results.js";
7
+ export * from "./runtime/evaluator.js";
8
+ export * from "./runtime/commands.js";
9
+ export * from "./runtime/runner.js";
10
+ export * from "./types.js";
11
+ export * from "./scene/types.js";
12
+ export * from "./scene/parser.js";
13
+ export * from "./react/useYarnRunner.js";
14
+ export * from "./react/DialogueView.js";
15
+ export * from "./react/DialogueExample.js";
16
+ export * from "./react/DialogueScene.js";
17
+
@@ -0,0 +1,93 @@
1
+ export type Position = { line: number; column: number };
2
+
3
+ export interface NodeHeaderMap {
4
+ [key: string]: string;
5
+ }
6
+
7
+ export interface YarnDocument {
8
+ type: "Document";
9
+ enums: EnumDefinition[];
10
+ nodes: YarnNode[];
11
+ }
12
+
13
+ export interface EnumDefinition {
14
+ type: "Enum";
15
+ name: string;
16
+ cases: string[];
17
+ }
18
+
19
+ export interface YarnNode {
20
+ type: "Node";
21
+ title: string;
22
+ headers: NodeHeaderMap;
23
+ nodeTags?: string[];
24
+ when?: string[]; // Array of when conditions (can be "once", "always", or expression like "$has_sword")
25
+ css?: string; // Custom CSS style for node
26
+ body: Statement[];
27
+ }
28
+
29
+ export type Statement =
30
+ | Line
31
+ | Command
32
+ | OptionGroup
33
+ | IfBlock
34
+ | OnceBlock
35
+ | Jump
36
+ | Detour
37
+ | EnumBlock;
38
+
39
+ export interface Line {
40
+ type: "Line";
41
+ speaker?: string;
42
+ text: string;
43
+ tags?: string[];
44
+ }
45
+
46
+ export interface Command {
47
+ type: "Command";
48
+ content: string; // inside << >>
49
+ }
50
+
51
+ export interface Jump {
52
+ type: "Jump";
53
+ target: string;
54
+ }
55
+
56
+ export interface Detour {
57
+ type: "Detour";
58
+ target: string;
59
+ }
60
+
61
+ export interface OptionGroup {
62
+ type: "OptionGroup";
63
+ options: Option[];
64
+ }
65
+
66
+ export interface Option {
67
+ type: "Option";
68
+ text: string;
69
+ body: Statement[]; // executed if chosen
70
+ tags?: string[];
71
+ css?: string; // Custom CSS style for option
72
+ }
73
+
74
+ export interface IfBlock {
75
+ type: "If";
76
+ branches: Array<{
77
+ condition: string | null; // null for else
78
+ body: Statement[];
79
+ }>;
80
+ }
81
+
82
+ export interface OnceBlock {
83
+ type: "Once";
84
+ body: Statement[];
85
+ }
86
+
87
+ export interface EnumBlock {
88
+ type: "Enum";
89
+ name: string;
90
+ cases: string[];
91
+ }
92
+
93
+
@@ -0,0 +1,108 @@
1
+ export interface Token {
2
+ type:
3
+ | "HEADER_KEY"
4
+ | "HEADER_VALUE"
5
+ | "NODE_START" // ---
6
+ | "NODE_END" // ===
7
+ | "OPTION" // ->
8
+ | "COMMAND" // <<...>> (single-line)
9
+ | "TEXT" // any non-empty content line
10
+ | "EMPTY"
11
+ | "INDENT"
12
+ | "DEDENT"
13
+ | "EOF";
14
+ text: string;
15
+ line: number;
16
+ column: number;
17
+ }
18
+
19
+ // Minimal indentation-sensitive lexer to support options and their bodies.
20
+ export function lex(input: string): Token[] {
21
+ const lines = input.replace(/\r\n?/g, "\n").split("\n");
22
+ const tokens: Token[] = [];
23
+ const indentStack: number[] = [0];
24
+
25
+ let inHeaders = true;
26
+
27
+ function push(type: Token["type"], text: string, line: number, column: number) {
28
+ tokens.push({ type, text, line, column });
29
+ }
30
+
31
+ for (let i = 0; i < lines.length; i++) {
32
+ const raw = lines[i];
33
+ const lineNum = i + 1;
34
+ const indent = raw.match(/^[ \t]*/)?.[0] ?? "";
35
+ const content = raw.slice(indent.length);
36
+
37
+ // Manage indentation tokens only within node bodies
38
+ if (!inHeaders) {
39
+ const prev = indentStack[indentStack.length - 1];
40
+ if (indent.length > prev) {
41
+ indentStack.push(indent.length);
42
+ push("INDENT", "", lineNum, 1);
43
+ } else if (indent.length < prev) {
44
+ while (indentStack.length && indent.length < indentStack[indentStack.length - 1]) {
45
+ indentStack.pop();
46
+ push("DEDENT", "", lineNum, 1);
47
+ }
48
+ }
49
+ }
50
+
51
+ if (content.trim() === "") {
52
+ push("EMPTY", "", lineNum, 1);
53
+ continue;
54
+ }
55
+
56
+ if (content === "---") {
57
+ inHeaders = false;
58
+ push("NODE_START", content, lineNum, indent.length + 1);
59
+ continue;
60
+ }
61
+ if (content === "===") {
62
+ inHeaders = true;
63
+ // flush indentation to root
64
+ while (indentStack.length > 1) {
65
+ indentStack.pop();
66
+ push("DEDENT", "", lineNum, 1);
67
+ }
68
+ push("NODE_END", content, lineNum, indent.length + 1);
69
+ continue;
70
+ }
71
+
72
+ // Header: key: value (only valid while inHeaders)
73
+ if (inHeaders) {
74
+ const m = content.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$/);
75
+ if (m) {
76
+ push("HEADER_KEY", m[1], lineNum, indent.length + 1);
77
+ push("HEADER_VALUE", m[2], lineNum, indent.length + 1 + m[0].indexOf(m[2]));
78
+ continue;
79
+ }
80
+ }
81
+
82
+ if (content.startsWith("->")) {
83
+ push("OPTION", content.slice(2).trim(), lineNum, indent.length + 1);
84
+ continue;
85
+ }
86
+
87
+ // Commands like <<...>> (single line)
88
+ const cmd = content.match(/^<<(.+?)>>\s*$/);
89
+ if (cmd) {
90
+ push("COMMAND", cmd[1].trim(), lineNum, indent.length + 1);
91
+ continue;
92
+ }
93
+
94
+ // Plain text line
95
+ push("TEXT", content, lineNum, indent.length + 1);
96
+ }
97
+
98
+ // close remaining indentation at EOF
99
+ while (indentStack.length > 1) {
100
+ indentStack.pop();
101
+ tokens.push({ type: "DEDENT", text: "", line: lines.length, column: 1 });
102
+ }
103
+
104
+ tokens.push({ type: "EOF", text: "", line: lines.length + 1, column: 1 });
105
+ return tokens;
106
+ }
107
+
108
+