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.
- package/README.md +34 -16
- package/dist/compile/compiler.js +2 -2
- package/dist/compile/compiler.js.map +1 -1
- package/dist/compile/ir.d.ts +1 -0
- package/dist/markup/parser.js +12 -2
- package/dist/markup/parser.js.map +1 -1
- package/dist/model/ast.d.ts +1 -0
- package/dist/parse/lexer.js +5 -5
- package/dist/parse/lexer.js.map +1 -1
- package/dist/parse/parser.js +12 -2
- package/dist/parse/parser.js.map +1 -1
- package/dist/react/DialogueExample.js +12 -10
- package/dist/react/DialogueExample.js.map +1 -1
- package/dist/react/DialogueView.d.ts +10 -4
- package/dist/react/DialogueView.js +38 -12
- package/dist/react/DialogueView.js.map +1 -1
- package/dist/react/MarkupRenderer.js +1 -1
- package/dist/react/MarkupRenderer.js.map +1 -1
- package/dist/react/useYarnRunner.js +63 -10
- package/dist/react/useYarnRunner.js.map +1 -1
- package/dist/runtime/evaluator.js +3 -2
- package/dist/runtime/evaluator.js.map +1 -1
- package/dist/runtime/runner.d.ts +2 -0
- package/dist/runtime/runner.js +66 -10
- package/dist/runtime/runner.js.map +1 -1
- package/dist/tests/dialogue_view.test.d.ts +1 -0
- package/dist/tests/dialogue_view.test.js +18 -0
- package/dist/tests/dialogue_view.test.js.map +1 -0
- package/dist/tests/markup.test.js +7 -0
- package/dist/tests/markup.test.js.map +1 -1
- package/dist/tests/options.test.js +164 -9
- package/dist/tests/options.test.js.map +1 -1
- package/dist/tests/variables_flow_cmds.test.js +52 -10
- package/dist/tests/variables_flow_cmds.test.js.map +1 -1
- package/docs/markup.md +33 -33
- package/eslint.config.cjs +39 -39
- package/package.json +6 -6
- package/src/compile/compiler.ts +2 -2
- package/src/compile/ir.ts +1 -1
- package/src/markup/parser.ts +53 -43
- package/src/model/ast.ts +1 -0
- package/src/parse/lexer.ts +18 -18
- package/src/parse/parser.ts +33 -22
- package/src/react/DialogueExample.tsx +16 -14
- package/src/react/DialogueView.tsx +312 -275
- package/src/react/MarkupRenderer.tsx +1 -2
- package/src/react/useYarnRunner.tsx +101 -34
- package/src/runtime/evaluator.ts +15 -14
- package/src/runtime/runner.ts +102 -37
- package/src/tests/dialogue_view.test.tsx +26 -0
- package/src/tests/markup.test.ts +17 -1
- package/src/tests/options.test.ts +206 -36
- package/src/tests/variables_flow_cmds.test.ts +72 -28
package/src/parse/lexer.ts
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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;
|
package/src/parse/parser.ts
CHANGED
|
@@ -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:
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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>
|