yarn-spinner-runner-ts 0.1.4 → 0.1.5

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 (53) hide show
  1. package/README.md +34 -16
  2. package/dist/compile/compiler.js +2 -2
  3. package/dist/compile/compiler.js.map +1 -1
  4. package/dist/compile/ir.d.ts +1 -0
  5. package/dist/markup/parser.js +12 -2
  6. package/dist/markup/parser.js.map +1 -1
  7. package/dist/model/ast.d.ts +1 -0
  8. package/dist/parse/lexer.js +5 -5
  9. package/dist/parse/lexer.js.map +1 -1
  10. package/dist/parse/parser.js +12 -2
  11. package/dist/parse/parser.js.map +1 -1
  12. package/dist/react/DialogueExample.js +12 -10
  13. package/dist/react/DialogueExample.js.map +1 -1
  14. package/dist/react/DialogueView.d.ts +10 -4
  15. package/dist/react/DialogueView.js +38 -12
  16. package/dist/react/DialogueView.js.map +1 -1
  17. package/dist/react/MarkupRenderer.js +1 -1
  18. package/dist/react/MarkupRenderer.js.map +1 -1
  19. package/dist/react/useYarnRunner.js +63 -10
  20. package/dist/react/useYarnRunner.js.map +1 -1
  21. package/dist/runtime/evaluator.js +3 -2
  22. package/dist/runtime/evaluator.js.map +1 -1
  23. package/dist/runtime/runner.d.ts +2 -0
  24. package/dist/runtime/runner.js +66 -10
  25. package/dist/runtime/runner.js.map +1 -1
  26. package/dist/tests/dialogue_view.test.d.ts +1 -0
  27. package/dist/tests/dialogue_view.test.js +18 -0
  28. package/dist/tests/dialogue_view.test.js.map +1 -0
  29. package/dist/tests/markup.test.js +7 -0
  30. package/dist/tests/markup.test.js.map +1 -1
  31. package/dist/tests/options.test.js +164 -9
  32. package/dist/tests/options.test.js.map +1 -1
  33. package/dist/tests/variables_flow_cmds.test.js +52 -10
  34. package/dist/tests/variables_flow_cmds.test.js.map +1 -1
  35. package/docs/markup.md +33 -33
  36. package/eslint.config.cjs +39 -39
  37. package/package.json +6 -6
  38. package/src/compile/compiler.ts +2 -2
  39. package/src/compile/ir.ts +1 -1
  40. package/src/markup/parser.ts +53 -43
  41. package/src/model/ast.ts +1 -0
  42. package/src/parse/lexer.ts +18 -18
  43. package/src/parse/parser.ts +33 -22
  44. package/src/react/DialogueExample.tsx +16 -14
  45. package/src/react/DialogueView.tsx +312 -275
  46. package/src/react/MarkupRenderer.tsx +1 -2
  47. package/src/react/useYarnRunner.tsx +101 -34
  48. package/src/runtime/evaluator.ts +15 -14
  49. package/src/runtime/runner.ts +102 -37
  50. package/src/tests/dialogue_view.test.tsx +26 -0
  51. package/src/tests/markup.test.ts +17 -1
  52. package/src/tests/options.test.ts +206 -36
  53. package/src/tests/variables_flow_cmds.test.ts +72 -28
@@ -34,24 +34,24 @@ export function lex(input: string): Token[] {
34
34
  const indent = raw.match(/^[ \t]*/)?.[0] ?? "";
35
35
  const content = raw.slice(indent.length);
36
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
- }
37
+ if (content.trim() === "") {
38
+ push("EMPTY", "", lineNum, 1);
39
+ continue;
40
+ }
41
+
42
+ // Manage indentation tokens only within node bodies and on non-empty lines
43
+ if (!inHeaders) {
44
+ const prev = indentStack[indentStack.length - 1];
45
+ if (indent.length > prev) {
46
+ indentStack.push(indent.length);
47
+ push("INDENT", "", lineNum, 1);
48
+ } else if (indent.length < prev) {
49
+ while (indentStack.length && indent.length < indentStack[indentStack.length - 1]) {
50
+ indentStack.pop();
51
+ push("DEDENT", "", lineNum, 1);
52
+ }
53
+ }
54
+ }
55
55
 
56
56
  if (content === "---") {
57
57
  inHeaders = false;
@@ -215,24 +215,26 @@ class Parser {
215
215
  // One or more OPTION lines, with bodies under INDENT
216
216
  while (this.at("OPTION")) {
217
217
  const raw = this.take("OPTION").text;
218
- const { cleanText: textWithAttrs, tags } = this.extractTags(raw);
219
- const { text: textWithoutCss, css } = this.extractCss(textWithAttrs);
220
- const markup = parseMarkup(textWithoutCss);
221
- let body: Statement[] = [];
222
- if (this.at("INDENT")) {
223
- this.take("INDENT");
224
- body = this.parseStatementsUntil("DEDENT");
218
+ const { cleanText: textWithAttrs, tags } = this.extractTags(raw);
219
+ const { text: textWithCondition, css } = this.extractCss(textWithAttrs);
220
+ const { text: optionText, condition } = this.extractOptionCondition(textWithCondition);
221
+ const markup = parseMarkup(optionText);
222
+ let body: Statement[] = [];
223
+ if (this.at("INDENT")) {
224
+ this.take("INDENT");
225
+ body = this.parseStatementsUntil("DEDENT");
225
226
  this.take("DEDENT");
226
227
  while (this.at("EMPTY")) this.i++;
227
228
  }
228
- options.push({
229
- type: "Option",
230
- text: markup.text,
231
- body,
232
- tags,
233
- css,
234
- markup: this.normalizeMarkup(markup),
235
- });
229
+ options.push({
230
+ type: "Option",
231
+ text: markup.text,
232
+ body,
233
+ tags,
234
+ css,
235
+ markup: this.normalizeMarkup(markup),
236
+ condition,
237
+ });
236
238
  // Consecutive options belong to the same group; break on non-OPTION
237
239
  while (this.at("EMPTY")) this.i++;
238
240
  }
@@ -283,15 +285,24 @@ class Parser {
283
285
  return { cleanText: input };
284
286
  }
285
287
 
286
- private extractCss(input: string): { text: string; css?: string } {
287
- const cssMatch = input.match(/\s*&css\{([^}]*)\}\s*$/);
288
- if (cssMatch) {
289
- const css = cssMatch[1].trim();
290
- const text = input.replace(cssMatch[0], "").trimEnd();
288
+ private extractCss(input: string): { text: string; css?: string } {
289
+ const cssMatch = input.match(/\s*&css\{([^}]*)\}\s*$/);
290
+ if (cssMatch) {
291
+ const css = cssMatch[1].trim();
292
+ const text = input.replace(cssMatch[0], "").trimEnd();
291
293
  return { text, css };
292
294
  }
293
- return { text: input };
294
- }
295
+ return { text: input };
296
+ }
297
+
298
+ private extractOptionCondition(input: string): { text: string; condition?: string } {
299
+ const match = input.match(/\s\[\s*if\s+([^\]]+)\]\s*$/i);
300
+ if (match) {
301
+ const text = input.slice(0, match.index).trimEnd();
302
+ return { text, condition: match[1].trim() };
303
+ }
304
+ return { text: input };
305
+ }
295
306
 
296
307
  private parseStatementsUntilStop(shouldStop: () => boolean): Statement[] {
297
308
  const out: Statement[] = [];
@@ -1,7 +1,6 @@
1
1
  import React, { useState, useMemo } from "react";
2
2
  import { parseYarn } from "../parse/parser.js";
3
3
  import { compile } from "../compile/compiler.js";
4
- import { useYarnRunner } from "./useYarnRunner.js";
5
4
  import { DialogueView } from "./DialogueView.js";
6
5
  import { parseScenes } from "../scene/parser.js";
7
6
  import type { SceneCollection } from "../scene/types.js";
@@ -9,11 +8,12 @@ import type { SceneCollection } from "../scene/types.js";
9
8
  const DEFAULT_YARN = `title: Start
10
9
  scene: scene1
11
10
  ---
12
- Narrator: [wave]hello[/wave] [b]hello[/b] baarter
13
- Narrator: Welcome to yarn-spinner-ts!
11
+ << declare $hasBadge = false >>
12
+ Narrator: Welcome to [b]yarn-spinner-ts[/b], {$playerName}!
13
+ Narrator: Current street cred: {$reputation}
14
14
  npc: This is a dialogue system powered by Yarn Spinner.
15
15
  Narrator: Click anywhere to continue, or choose an option below.
16
- -> Start the adventure &css{backgroundColor: #4a9eff; color: white;}
16
+ -> Start the adventure &css{backgroundColor: #4a9eff; color: white;} [if $hasBadge]
17
17
  Narrator: Great! Let's begin your journey.
18
18
  <<jump NextScene>>
19
19
  -> Learn more &css{backgroundColor: #2ecc71; color: red;}
@@ -47,7 +47,7 @@ actors:
47
47
  export function DialogueExample() {
48
48
  const [yarnText] = useState(DEFAULT_YARN);
49
49
  const [error, setError] = useState<string | null>(null);
50
- const enableTypingAnimation = true;
50
+ const enableTypingAnimation = false;
51
51
 
52
52
  const scenes: SceneCollection = useMemo(() => {
53
53
  try {
@@ -69,13 +69,10 @@ export function DialogueExample() {
69
69
  }
70
70
  }, [yarnText]);
71
71
 
72
- const { result, advance } = useYarnRunner(
73
- program || { nodes: {}, enums: {} },
74
- {
75
- startAt: "Start",
76
- variables: {},
77
- }
78
- );
72
+ const customFunctions = useMemo(() => ({
73
+ greet: () => {console.log('test')},
74
+ double: (num: unknown) => Number(num) * 2
75
+ }), []);
79
76
 
80
77
  return (
81
78
  <div
@@ -106,9 +103,10 @@ export function DialogueExample() {
106
103
  )}
107
104
 
108
105
  <DialogueView
109
- result={result}
110
- onAdvance={advance}
106
+ program={program || { nodes: {}, enums: {} }}
107
+ startNode="Start"
111
108
  scenes={scenes}
109
+ variables={{ playerName: "V", reputation: 3 }}
112
110
  enableTypingAnimation={enableTypingAnimation}
113
111
  showTypingCursor={true}
114
112
  typingSpeed={20}
@@ -117,6 +115,10 @@ export function DialogueExample() {
117
115
  autoAdvanceDelay={2000}
118
116
  actorTransitionDuration={1000}
119
117
  pauseBeforeAdvance={enableTypingAnimation ? 1000 : 0}
118
+ onStoryEnd={(info) => {
119
+ console.log('Story ended with variables:', info.variables);
120
+ }}
121
+ functions={customFunctions}
120
122
  />
121
123
  </div>
122
124
  </div>