yarn-spinner-runner-ts 0.1.2 → 0.1.4-a

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 (84) hide show
  1. package/README.md +102 -88
  2. package/dist/compile/compiler.js +4 -4
  3. package/dist/compile/compiler.js.map +1 -1
  4. package/dist/compile/ir.d.ts +3 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.js +3 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/markup/parser.d.ts +3 -0
  9. package/dist/markup/parser.js +332 -0
  10. package/dist/markup/parser.js.map +1 -0
  11. package/dist/markup/types.d.ts +17 -0
  12. package/dist/markup/types.js +2 -0
  13. package/dist/markup/types.js.map +1 -0
  14. package/dist/model/ast.d.ts +3 -0
  15. package/dist/parse/parser.js +57 -8
  16. package/dist/parse/parser.js.map +1 -1
  17. package/dist/react/DialogueExample.js +13 -10
  18. package/dist/react/DialogueExample.js.map +1 -1
  19. package/dist/react/DialogueScene.d.ts +2 -1
  20. package/dist/react/DialogueScene.js +95 -26
  21. package/dist/react/DialogueScene.js.map +1 -1
  22. package/dist/react/DialogueView.d.ts +18 -4
  23. package/dist/react/DialogueView.js +84 -7
  24. package/dist/react/DialogueView.js.map +1 -1
  25. package/dist/react/MarkupRenderer.d.ts +8 -0
  26. package/dist/react/MarkupRenderer.js +64 -0
  27. package/dist/react/MarkupRenderer.js.map +1 -0
  28. package/dist/react/TypingText.d.ts +14 -0
  29. package/dist/react/TypingText.js +78 -0
  30. package/dist/react/TypingText.js.map +1 -0
  31. package/dist/react/useYarnRunner.js +10 -1
  32. package/dist/react/useYarnRunner.js.map +1 -1
  33. package/dist/runtime/commands.js +12 -1
  34. package/dist/runtime/commands.js.map +1 -1
  35. package/dist/runtime/results.d.ts +3 -0
  36. package/dist/runtime/runner.d.ts +7 -0
  37. package/dist/runtime/runner.js +161 -14
  38. package/dist/runtime/runner.js.map +1 -1
  39. package/dist/tests/custom_functions.test.d.ts +1 -0
  40. package/dist/tests/custom_functions.test.js +129 -0
  41. package/dist/tests/custom_functions.test.js.map +1 -0
  42. package/dist/tests/markup.test.d.ts +1 -0
  43. package/dist/tests/markup.test.js +46 -0
  44. package/dist/tests/markup.test.js.map +1 -0
  45. package/dist/tests/nodes_lines.test.js +25 -1
  46. package/dist/tests/nodes_lines.test.js.map +1 -1
  47. package/dist/tests/options.test.js +30 -1
  48. package/dist/tests/options.test.js.map +1 -1
  49. package/dist/tests/story_end.test.d.ts +1 -0
  50. package/dist/tests/story_end.test.js +37 -0
  51. package/dist/tests/story_end.test.js.map +1 -0
  52. package/dist/tests/typing-text.test.d.ts +1 -0
  53. package/dist/tests/typing-text.test.js +12 -0
  54. package/dist/tests/typing-text.test.js.map +1 -0
  55. package/docs/actor-transition.md +34 -0
  56. package/docs/markup.md +34 -19
  57. package/docs/scenes-actors-setup.md +1 -0
  58. package/docs/typing-animation.md +44 -0
  59. package/eslint.config.cjs +3 -0
  60. package/examples/browser/index.html +1 -1
  61. package/examples/browser/main.tsx +0 -2
  62. package/package.json +1 -1
  63. package/src/compile/compiler.ts +4 -4
  64. package/src/compile/ir.ts +3 -2
  65. package/src/index.ts +3 -0
  66. package/src/markup/parser.ts +372 -0
  67. package/src/markup/types.ts +22 -0
  68. package/src/model/ast.ts +17 -13
  69. package/src/parse/parser.ts +60 -8
  70. package/src/react/DialogueExample.tsx +27 -51
  71. package/src/react/DialogueScene.tsx +143 -44
  72. package/src/react/DialogueView.tsx +150 -14
  73. package/src/react/MarkupRenderer.tsx +110 -0
  74. package/src/react/TypingText.tsx +127 -0
  75. package/src/react/dialogue.css +26 -13
  76. package/src/react/useYarnRunner.tsx +13 -1
  77. package/src/runtime/commands.ts +14 -1
  78. package/src/runtime/results.ts +3 -1
  79. package/src/runtime/runner.ts +170 -14
  80. package/src/tests/custom_functions.test.ts +140 -0
  81. package/src/tests/markup.test.ts +62 -0
  82. package/src/tests/nodes_lines.test.ts +35 -1
  83. package/src/tests/options.test.ts +39 -1
  84. package/src/tests/story_end.test.ts +42 -0
@@ -0,0 +1,372 @@
1
+ import type { MarkupParseResult, MarkupSegment, MarkupValue, MarkupWrapper } from "./types.js";
2
+
3
+ const DEFAULT_HTML_TAGS = new Set(["b", "em", "small", "strong", "sub", "sup", "ins", "del", "mark"]);
4
+
5
+ interface StackEntry {
6
+ name: string;
7
+ type: MarkupWrapper["type"];
8
+ properties: Record<string, MarkupValue>;
9
+ originalText: string;
10
+ }
11
+
12
+ interface ParsedTag {
13
+ kind: "open" | "close" | "self";
14
+ name: string;
15
+ properties: Record<string, MarkupValue>;
16
+ }
17
+
18
+ const SELF_CLOSING_SPACE_REGEX = /\s+\/$/;
19
+ const ATTRIBUTE_REGEX =
20
+ /^([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"']+)))?/;
21
+
22
+ export function parseMarkup(input: string): MarkupParseResult {
23
+ const segments: MarkupSegment[] = [];
24
+ const stack: StackEntry[] = [];
25
+ const chars: string[] = [];
26
+ let currentSegment: MarkupSegment | null = null;
27
+ let nomarkupDepth = 0;
28
+
29
+ const pushSegment = (segment: MarkupSegment) => {
30
+ if (segment.selfClosing || segment.end > segment.start) {
31
+ segments.push(segment);
32
+ }
33
+ };
34
+
35
+ const wrappersEqual = (a: MarkupWrapper[], b: MarkupWrapper[]) => {
36
+ if (a.length !== b.length) return false;
37
+ for (let i = 0; i < a.length; i++) {
38
+ const wa = a[i];
39
+ const wb = b[i];
40
+ if (wa.name !== wb.name || wa.type !== wb.type) return false;
41
+ const keysA = Object.keys(wa.properties);
42
+ const keysB = Object.keys(wb.properties);
43
+ if (keysA.length !== keysB.length) return false;
44
+ for (const key of keysA) {
45
+ if (wa.properties[key] !== wb.properties[key]) return false;
46
+ }
47
+ }
48
+ return true;
49
+ };
50
+
51
+ const flushCurrentSegment = () => {
52
+ if (currentSegment) {
53
+ segments.push(currentSegment);
54
+ currentSegment = null;
55
+ }
56
+ };
57
+
58
+ const cloneWrappers = (): MarkupWrapper[] =>
59
+ stack.map((entry) => ({
60
+ name: entry.name,
61
+ type: entry.type,
62
+ properties: { ...entry.properties },
63
+ }));
64
+
65
+ const appendChar = (char: string) => {
66
+ const index = chars.length;
67
+ chars.push(char);
68
+ const wrappers = cloneWrappers();
69
+ if (currentSegment && wrappersEqual(currentSegment.wrappers, wrappers)) {
70
+ currentSegment.end = index + 1;
71
+ } else {
72
+ flushCurrentSegment();
73
+ currentSegment = {
74
+ start: index,
75
+ end: index + 1,
76
+ wrappers,
77
+ };
78
+ }
79
+ };
80
+
81
+ const appendLiteral = (literal: string) => {
82
+ for (const ch of literal) {
83
+ appendChar(ch);
84
+ }
85
+ };
86
+
87
+ const parseTag = (contentRaw: string): ParsedTag | null => {
88
+ let content = contentRaw.trim();
89
+ if (!content) return null;
90
+
91
+ if (content.startsWith("/")) {
92
+ const name = content.slice(1).trim().toLowerCase();
93
+ if (!name) return null;
94
+ return { kind: "close", name, properties: {} };
95
+ }
96
+
97
+ let kind: ParsedTag["kind"] = "open";
98
+ if (content.endsWith("/")) {
99
+ content = content.replace(SELF_CLOSING_SPACE_REGEX, "").trim();
100
+ if (content.endsWith("/")) {
101
+ content = content.slice(0, -1).trim();
102
+ }
103
+ kind = "self";
104
+ }
105
+
106
+ const nameMatch = content.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)/);
107
+ if (!nameMatch) return null;
108
+ const name = nameMatch[1].toLowerCase();
109
+ let rest = content.slice(nameMatch[0].length).trim();
110
+
111
+ const properties: Record<string, MarkupValue> = {};
112
+ while (rest.length > 0) {
113
+ const attrMatch = rest.match(ATTRIBUTE_REGEX);
114
+ if (!attrMatch) {
115
+ break;
116
+ }
117
+ const [, keyRaw, doubleQuoted, singleQuoted, bare] = attrMatch;
118
+ const key = keyRaw.toLowerCase();
119
+ let value: MarkupValue = true;
120
+ const rawValue = doubleQuoted ?? singleQuoted ?? bare;
121
+ if (rawValue !== undefined) {
122
+ value = parseAttributeValue(rawValue);
123
+ }
124
+ properties[key] = value;
125
+ rest = rest.slice(attrMatch[0].length).trim();
126
+ }
127
+
128
+ return { kind, name, properties };
129
+ };
130
+
131
+ const parseAttributeValue = (raw: string): MarkupValue => {
132
+ const trimmed = raw.trim();
133
+ if (/^(true|false)$/i.test(trimmed)) {
134
+ return /^true$/i.test(trimmed);
135
+ }
136
+ if (/^[+-]?\d+(\.\d+)?$/.test(trimmed)) {
137
+ const num = Number(trimmed);
138
+ if (!Number.isNaN(num)) {
139
+ return num;
140
+ }
141
+ }
142
+ return trimmed;
143
+ };
144
+
145
+ const handleSelfClosing = (tag: ParsedTag) => {
146
+ const wrapper: MarkupWrapper = {
147
+ name: tag.name,
148
+ type: DEFAULT_HTML_TAGS.has(tag.name) ? "default" : "custom",
149
+ properties: tag.properties,
150
+ };
151
+ const position = chars.length;
152
+ pushSegment({
153
+ start: position,
154
+ end: position,
155
+ wrappers: [wrapper],
156
+ selfClosing: true,
157
+ });
158
+ };
159
+
160
+ let i = 0;
161
+ while (i < input.length) {
162
+ const char = input[i];
163
+ if (char === "\\" && i + 1 < input.length) {
164
+ const next = input[i + 1];
165
+ if (next === "[" || next === "]" || next === "\\") {
166
+ appendChar(next);
167
+ i += 2;
168
+ continue;
169
+ }
170
+ }
171
+
172
+ if (char === "[") {
173
+ const closeIndex = findClosingBracket(input, i + 1);
174
+ if (closeIndex === -1) {
175
+ appendChar(char);
176
+ i += 1;
177
+ continue;
178
+ }
179
+ const content = input.slice(i + 1, closeIndex);
180
+ const originalText = input.slice(i, closeIndex + 1);
181
+
182
+ const parsed = parseTag(content);
183
+ if (!parsed) {
184
+ appendLiteral(originalText);
185
+ i = closeIndex + 1;
186
+ continue;
187
+ }
188
+
189
+ if (parsed.name === "nomarkup") {
190
+ if (parsed.kind === "open") {
191
+ nomarkupDepth += 1;
192
+ } else if (parsed.kind === "close" && nomarkupDepth > 0) {
193
+ nomarkupDepth -= 1;
194
+ }
195
+ i = closeIndex + 1;
196
+ continue;
197
+ }
198
+
199
+ if (nomarkupDepth > 0) {
200
+ appendLiteral(originalText);
201
+ i = closeIndex + 1;
202
+ continue;
203
+ }
204
+
205
+ if (parsed.kind === "open") {
206
+ const entry: StackEntry = {
207
+ name: parsed.name,
208
+ type: DEFAULT_HTML_TAGS.has(parsed.name) ? "default" : "custom",
209
+ properties: parsed.properties,
210
+ originalText,
211
+ };
212
+ stack.push(entry);
213
+ flushCurrentSegment();
214
+ i = closeIndex + 1;
215
+ continue;
216
+ }
217
+
218
+ if (parsed.kind === "self") {
219
+ handleSelfClosing(parsed);
220
+ i = closeIndex + 1;
221
+ continue;
222
+ }
223
+
224
+ // closing tag
225
+ if (stack.length === 0) {
226
+ appendLiteral(originalText);
227
+ i = closeIndex + 1;
228
+ continue;
229
+ }
230
+ const top = stack[stack.length - 1];
231
+ if (top.name === parsed.name) {
232
+ flushCurrentSegment();
233
+ stack.pop();
234
+ i = closeIndex + 1;
235
+ continue;
236
+ }
237
+ // mismatched closing; treat as literal
238
+ appendLiteral(originalText);
239
+ i = closeIndex + 1;
240
+ continue;
241
+ }
242
+
243
+ appendChar(char);
244
+ i += 1;
245
+ }
246
+
247
+ flushCurrentSegment();
248
+
249
+ // If any tags remain open, treat them as literal text appended at end
250
+ while (stack.length > 0) {
251
+ const entry = stack.pop()!;
252
+ appendLiteral(entry.originalText);
253
+ }
254
+ flushCurrentSegment();
255
+
256
+ const text = chars.join("");
257
+ return {
258
+ text,
259
+ segments: mergeSegments(segments, text.length),
260
+ };
261
+ }
262
+
263
+ function mergeSegments(segments: MarkupSegment[], textLength: number): MarkupSegment[] {
264
+ const sorted = [...segments].sort((a, b) => a.start - b.start || a.end - b.end);
265
+ const merged: MarkupSegment[] = [];
266
+ let last: MarkupSegment | null = null;
267
+
268
+ for (const seg of sorted) {
269
+ if (seg.start === seg.end && !seg.selfClosing) {
270
+ continue;
271
+ }
272
+ if (last && !seg.selfClosing && last.end === seg.start && wrappersMatch(last.wrappers, seg.wrappers)) {
273
+ last.end = seg.end;
274
+ } else {
275
+ last = {
276
+ start: seg.start,
277
+ end: seg.end,
278
+ wrappers: seg.wrappers,
279
+ selfClosing: seg.selfClosing,
280
+ };
281
+ merged.push(last);
282
+ }
283
+ }
284
+
285
+ if (merged.length === 0 && textLength > 0) {
286
+ merged.push({
287
+ start: 0,
288
+ end: textLength,
289
+ wrappers: [],
290
+ });
291
+ }
292
+
293
+ return merged;
294
+ }
295
+
296
+ function wrappersMatch(a: MarkupWrapper[], b: MarkupWrapper[]): boolean {
297
+ if (a.length !== b.length) return false;
298
+ for (let i = 0; i < a.length; i++) {
299
+ if (a[i].name !== b[i].name || a[i].type !== b[i].type) return false;
300
+ const keysA = Object.keys(a[i].properties);
301
+ const keysB = Object.keys(b[i].properties);
302
+ if (keysA.length !== keysB.length) return false;
303
+ for (const key of keysA) {
304
+ if (a[i].properties[key] !== b[i].properties[key]) return false;
305
+ }
306
+ }
307
+ return true;
308
+ }
309
+
310
+ function findClosingBracket(text: string, start: number): number {
311
+ for (let i = start; i < text.length; i++) {
312
+ if (text[i] === "]") {
313
+ let backslashCount = 0;
314
+ let j = i - 1;
315
+ while (j >= 0 && text[j] === "\\") {
316
+ backslashCount++;
317
+ j--;
318
+ }
319
+ if (backslashCount % 2 === 0) {
320
+ return i;
321
+ }
322
+ }
323
+ }
324
+ return -1;
325
+ }
326
+
327
+ export function sliceMarkup(result: MarkupParseResult, start: number, end?: number): MarkupParseResult {
328
+ const textLength = result.text.length;
329
+ const sliceStart = Math.max(0, Math.min(start, textLength));
330
+ const sliceEnd = end === undefined ? textLength : Math.max(sliceStart, Math.min(end, textLength));
331
+ const slicedSegments: MarkupSegment[] = [];
332
+
333
+ for (const seg of result.segments) {
334
+ const segStart = Math.max(seg.start, sliceStart);
335
+ const segEnd = Math.min(seg.end, sliceEnd);
336
+ if (seg.selfClosing) {
337
+ if (segStart >= sliceStart && segStart <= sliceEnd) {
338
+ slicedSegments.push({
339
+ start: segStart - sliceStart,
340
+ end: segStart - sliceStart,
341
+ wrappers: seg.wrappers,
342
+ selfClosing: true,
343
+ });
344
+ }
345
+ continue;
346
+ }
347
+ if (segEnd <= segStart) continue;
348
+ slicedSegments.push({
349
+ start: segStart - sliceStart,
350
+ end: segEnd - sliceStart,
351
+ wrappers: seg.wrappers.map((wrapper) => ({
352
+ name: wrapper.name,
353
+ type: wrapper.type,
354
+ properties: { ...wrapper.properties },
355
+ })),
356
+ });
357
+ }
358
+
359
+ if (slicedSegments.length === 0 && sliceEnd - sliceStart > 0) {
360
+ slicedSegments.push({
361
+ start: 0,
362
+ end: sliceEnd - sliceStart,
363
+ wrappers: [],
364
+ });
365
+ }
366
+
367
+ return {
368
+ text: result.text.slice(sliceStart, sliceEnd),
369
+ segments: mergeSegments(slicedSegments, sliceEnd - sliceStart),
370
+ };
371
+ }
372
+
@@ -0,0 +1,22 @@
1
+ export type MarkupValue = string | number | boolean;
2
+
3
+ export type MarkupWrapperType = "default" | "custom";
4
+
5
+ export interface MarkupWrapper {
6
+ name: string;
7
+ type: MarkupWrapperType;
8
+ properties: Record<string, MarkupValue>;
9
+ }
10
+
11
+ export interface MarkupSegment {
12
+ start: number;
13
+ end: number;
14
+ wrappers: MarkupWrapper[];
15
+ selfClosing?: boolean;
16
+ }
17
+
18
+ export interface MarkupParseResult {
19
+ text: string;
20
+ segments: MarkupSegment[];
21
+ }
22
+
package/src/model/ast.ts CHANGED
@@ -36,12 +36,15 @@ export type Statement =
36
36
  | Detour
37
37
  | EnumBlock;
38
38
 
39
- export interface Line {
40
- type: "Line";
41
- speaker?: string;
42
- text: string;
43
- tags?: string[];
44
- }
39
+ import type { MarkupParseResult } from "../markup/types.js";
40
+
41
+ export interface Line {
42
+ type: "Line";
43
+ speaker?: string;
44
+ text: string;
45
+ tags?: string[];
46
+ markup?: MarkupParseResult;
47
+ }
45
48
 
46
49
  export interface Command {
47
50
  type: "Command";
@@ -63,13 +66,14 @@ export interface OptionGroup {
63
66
  options: Option[];
64
67
  }
65
68
 
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
- }
69
+ export interface Option {
70
+ type: "Option";
71
+ text: string;
72
+ body: Statement[]; // executed if chosen
73
+ tags?: string[];
74
+ css?: string; // Custom CSS style for option
75
+ markup?: MarkupParseResult;
76
+ }
73
77
 
74
78
  export interface IfBlock {
75
79
  type: "If";
@@ -1,4 +1,6 @@
1
1
  import { lex, Token } from "./lexer.js";
2
+ import { parseMarkup, sliceMarkup } from "../markup/parser.js";
3
+ import type { MarkupParseResult } from "../markup/types.js";
2
4
  import type {
3
5
  YarnDocument,
4
6
  YarnNode,
@@ -177,17 +179,33 @@ class Parser {
177
179
  }
178
180
  if (t.type === "TEXT") {
179
181
  const raw = this.take("TEXT").text;
180
- const { cleanText: text, tags } = this.extractTags(raw);
181
- const speakerMatch = text.match(/^([^:\s][^:]*)\s*:\s*(.*)$/);
182
+ const { cleanText: textWithoutTags, tags } = this.extractTags(raw);
183
+ const markup = parseMarkup(textWithoutTags);
184
+ const speakerMatch = markup.text.match(/^([^:\s][^:]*)\s*:\s*(.*)$/);
182
185
  if (speakerMatch) {
183
- return { type: "Line", speaker: speakerMatch[1].trim(), text: speakerMatch[2], tags } as Line;
186
+ const messageText = speakerMatch[2];
187
+ const messageOffset = markup.text.length - messageText.length;
188
+ const slicedMarkup = sliceMarkup(markup, messageOffset);
189
+ const normalizedMarkup = this.normalizeMarkup(slicedMarkup);
190
+ return {
191
+ type: "Line",
192
+ speaker: speakerMatch[1].trim(),
193
+ text: messageText,
194
+ tags,
195
+ markup: normalizedMarkup,
196
+ } as Line;
184
197
  }
185
198
  // If/Else blocks use inline markup {if ...}
186
- const trimmed = text.trim();
199
+ const trimmed = markup.text.trim();
187
200
  if (trimmed.startsWith("{if ") || trimmed === "{else}" || trimmed.startsWith("{else if ") || trimmed === "{endif}") {
188
- return this.parseIfFromText(text);
201
+ return this.parseIfFromText(markup.text);
189
202
  }
190
- return { type: "Line", text, tags } as Line;
203
+ return {
204
+ type: "Line",
205
+ text: markup.text,
206
+ tags,
207
+ markup: this.normalizeMarkup(markup),
208
+ } as Line;
191
209
  }
192
210
  throw new ParseError(`Unexpected token ${t.type}`);
193
211
  }
@@ -198,7 +216,8 @@ class Parser {
198
216
  while (this.at("OPTION")) {
199
217
  const raw = this.take("OPTION").text;
200
218
  const { cleanText: textWithAttrs, tags } = this.extractTags(raw);
201
- const { text, css } = this.extractCss(textWithAttrs);
219
+ const { text: textWithoutCss, css } = this.extractCss(textWithAttrs);
220
+ const markup = parseMarkup(textWithoutCss);
202
221
  let body: Statement[] = [];
203
222
  if (this.at("INDENT")) {
204
223
  this.take("INDENT");
@@ -206,13 +225,46 @@ class Parser {
206
225
  this.take("DEDENT");
207
226
  while (this.at("EMPTY")) this.i++;
208
227
  }
209
- options.push({ type: "Option", text, body, tags, css });
228
+ options.push({
229
+ type: "Option",
230
+ text: markup.text,
231
+ body,
232
+ tags,
233
+ css,
234
+ markup: this.normalizeMarkup(markup),
235
+ });
210
236
  // Consecutive options belong to the same group; break on non-OPTION
211
237
  while (this.at("EMPTY")) this.i++;
212
238
  }
213
239
  return { type: "OptionGroup", options };
214
240
  }
215
241
 
242
+ private normalizeMarkup(result: MarkupParseResult): MarkupParseResult | undefined {
243
+ if (!result) return undefined;
244
+ if (result.segments.length === 0) {
245
+ return undefined;
246
+ }
247
+ const hasFormatting = result.segments.some(
248
+ (segment) => segment.wrappers.length > 0 || segment.selfClosing
249
+ );
250
+ if (!hasFormatting) {
251
+ return undefined;
252
+ }
253
+ return {
254
+ text: result.text,
255
+ segments: result.segments.map((segment) => ({
256
+ start: segment.start,
257
+ end: segment.end,
258
+ wrappers: segment.wrappers.map((wrapper) => ({
259
+ name: wrapper.name,
260
+ type: wrapper.type,
261
+ properties: { ...wrapper.properties },
262
+ })),
263
+ selfClosing: segment.selfClosing,
264
+ })),
265
+ };
266
+ }
267
+
216
268
  private extractTags(input: string): { cleanText: string; tags?: string[] } {
217
269
  const tags: string[] = [];
218
270
  // Match tags that are space-separated and not part of hex colors or CSS
@@ -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,8 +8,9 @@ import type { SceneCollection } from "../scene/types.js";
9
8
  const DEFAULT_YARN = `title: Start
10
9
  scene: scene1
11
10
  ---
12
- Narrator: Welcome to yarn-spinner-ts!
13
- Narrator: This is a dialogue system powered by Yarn Spinner.
11
+ Narrator: {greet()}
12
+ Narrator: Welcome to [b]yarn-spinner-ts[/b]!
13
+ npc: This is a dialogue system powered by Yarn Spinner.
14
14
  Narrator: Click anywhere to continue, or choose an option below.
15
15
  -> Start the adventure &css{backgroundColor: #4a9eff; color: white;}
16
16
  Narrator: Great! Let's begin your journey.
@@ -23,7 +23,7 @@ Narrator: Click anywhere to continue, or choose an option below.
23
23
 
24
24
  title: NextScene
25
25
  ---
26
- blablabla
26
+ npc: blablabla
27
27
  Narrator: You've reached the next scene!
28
28
  Narrator: The dialogue system supports rich features like:
29
29
  Narrator: • Variables and expressions
@@ -44,8 +44,9 @@ actors:
44
44
  `;
45
45
 
46
46
  export function DialogueExample() {
47
- const [yarnText, setYarnText] = useState(DEFAULT_YARN);
47
+ const [yarnText] = useState(DEFAULT_YARN);
48
48
  const [error, setError] = useState<string | null>(null);
49
+ const enableTypingAnimation = true;
49
50
 
50
51
  const scenes: SceneCollection = useMemo(() => {
51
52
  try {
@@ -67,13 +68,10 @@ export function DialogueExample() {
67
68
  }
68
69
  }, [yarnText]);
69
70
 
70
- const { result, advance } = useYarnRunner(
71
- program || { nodes: {}, enums: {} },
72
- {
73
- startAt: "Start",
74
- variables: {},
75
- }
76
- );
71
+ const customFunctions = useMemo(() => ({
72
+ greet: () => {console.log('test')},
73
+ double: (num: unknown) => Number(num) * 2
74
+ }), []);
77
75
 
78
76
  return (
79
77
  <div
@@ -103,45 +101,23 @@ export function DialogueExample() {
103
101
  </div>
104
102
  )}
105
103
 
106
- <DialogueView result={result} onAdvance={advance} scenes={scenes} />
107
-
108
- {/* <details style={{ marginTop: "30px", color: "#ffffff" }}>
109
- <summary style={{ cursor: "pointer", padding: "10px", backgroundColor: "rgba(74, 158, 255, 0.2)", borderRadius: "8px" }}>
110
- Edit Yarn Script
111
- </summary>
112
- <textarea
113
- value={yarnText}
114
- onChange={(e) => setYarnText(e.target.value)}
115
- style={{
116
- width: "100%",
117
- minHeight: "300px",
118
- marginTop: "10px",
119
- padding: "12px",
120
- fontFamily: "monospace",
121
- fontSize: "14px",
122
- backgroundColor: "#2a2a3e",
123
- color: "#ffffff",
124
- border: "1px solid #4a9eff",
125
- borderRadius: "8px",
126
- }}
127
- spellCheck={false}
128
- />
129
- <button
130
- onClick={() => window.location.reload()}
131
- style={{
132
- marginTop: "10px",
133
- padding: "10px 20px",
134
- backgroundColor: "#4a9eff",
135
- color: "#ffffff",
136
- border: "none",
137
- borderRadius: "8px",
138
- cursor: "pointer",
139
- fontSize: "16px",
140
- }}
141
- >
142
- Reload to Restart
143
- </button>
144
- </details> */}
104
+ <DialogueView
105
+ program={program || { nodes: {}, enums: {} }}
106
+ startNode="Start"
107
+ scenes={scenes}
108
+ enableTypingAnimation={enableTypingAnimation}
109
+ showTypingCursor={true}
110
+ typingSpeed={20}
111
+ cursorCharacter="$"
112
+ autoAdvanceAfterTyping={true}
113
+ autoAdvanceDelay={2000}
114
+ actorTransitionDuration={1000}
115
+ pauseBeforeAdvance={enableTypingAnimation ? 1000 : 0}
116
+ onStoryEnd={(info) => {
117
+ console.log('Story ended with variables:', info.variables);
118
+ }}
119
+ functions={customFunctions}
120
+ />
145
121
  </div>
146
122
  </div>
147
123
  );