yarn-spinner-runner-ts 0.1.2-b → 0.1.3
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 +5 -2
- package/dist/compile/compiler.js +4 -4
- package/dist/compile/compiler.js.map +1 -1
- package/dist/compile/ir.d.ts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/markup/parser.d.ts +3 -0
- package/dist/markup/parser.js +332 -0
- package/dist/markup/parser.js.map +1 -0
- package/dist/markup/types.d.ts +17 -0
- package/dist/markup/types.js +2 -0
- package/dist/markup/types.js.map +1 -0
- package/dist/model/ast.d.ts +3 -0
- package/dist/parse/parser.js +57 -8
- package/dist/parse/parser.js.map +1 -1
- package/dist/react/DialogueExample.js +4 -3
- package/dist/react/DialogueExample.js.map +1 -1
- package/dist/react/DialogueScene.d.ts +2 -1
- package/dist/react/DialogueScene.js +95 -26
- package/dist/react/DialogueScene.js.map +1 -1
- package/dist/react/DialogueView.d.ts +2 -1
- package/dist/react/DialogueView.js +5 -4
- package/dist/react/DialogueView.js.map +1 -1
- package/dist/react/MarkupRenderer.d.ts +8 -0
- package/dist/react/MarkupRenderer.js +64 -0
- package/dist/react/MarkupRenderer.js.map +1 -0
- package/dist/react/TypingText.d.ts +3 -1
- package/dist/react/TypingText.js +16 -40
- package/dist/react/TypingText.js.map +1 -1
- package/dist/runtime/results.d.ts +3 -0
- package/dist/runtime/runner.d.ts +1 -0
- package/dist/runtime/runner.js +151 -14
- package/dist/runtime/runner.js.map +1 -1
- package/dist/tests/markup.test.d.ts +1 -0
- package/dist/tests/markup.test.js +46 -0
- package/dist/tests/markup.test.js.map +1 -0
- package/dist/tests/nodes_lines.test.js +25 -1
- package/dist/tests/nodes_lines.test.js.map +1 -1
- package/dist/tests/options.test.js +30 -1
- package/dist/tests/options.test.js.map +1 -1
- package/docs/actor-transition.md +34 -0
- package/docs/markup.md +34 -19
- package/docs/scenes-actors-setup.md +1 -0
- package/package.json +1 -1
- package/src/compile/compiler.ts +4 -4
- package/src/compile/ir.ts +3 -2
- package/src/index.ts +3 -0
- package/src/markup/parser.ts +372 -0
- package/src/markup/types.ts +22 -0
- package/src/model/ast.ts +17 -13
- package/src/parse/parser.ts +60 -8
- package/src/react/DialogueExample.tsx +4 -2
- package/src/react/DialogueScene.tsx +143 -44
- package/src/react/DialogueView.tsx +19 -5
- package/src/react/MarkupRenderer.tsx +110 -0
- package/src/react/TypingText.tsx +25 -30
- package/src/react/dialogue.css +26 -13
- package/src/runtime/results.ts +3 -1
- package/src/runtime/runner.ts +158 -14
- package/src/tests/markup.test.ts +62 -0
- package/src/tests/nodes_lines.test.ts +35 -1
- package/src/tests/options.test.ts +39 -1
package/src/react/dialogue.css
CHANGED
|
@@ -43,19 +43,32 @@
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/* Actor image */
|
|
46
|
-
.yd-actor {
|
|
47
|
-
position: absolute;
|
|
48
|
-
top: 0;
|
|
49
|
-
left: 50%;
|
|
50
|
-
transform: translateX(-50%);
|
|
51
|
-
max-height: 70%;
|
|
52
|
-
max-width: 40%;
|
|
53
|
-
object-fit: contain;
|
|
54
|
-
|
|
55
|
-
transition: opacity 0.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
46
|
+
.yd-actor {
|
|
47
|
+
position: absolute;
|
|
48
|
+
top: 0;
|
|
49
|
+
left: 50%;
|
|
50
|
+
transform: translateX(-50%);
|
|
51
|
+
max-height: 70%;
|
|
52
|
+
max-width: 40%;
|
|
53
|
+
object-fit: contain;
|
|
54
|
+
opacity: 0;
|
|
55
|
+
transition: opacity var(--yd-actor-transition, 0.35s) ease-in-out;
|
|
56
|
+
pointer-events: none;
|
|
57
|
+
z-index: 1;
|
|
58
|
+
will-change: opacity;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.yd-actor--current {
|
|
62
|
+
z-index: 2;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.yd-actor--previous {
|
|
66
|
+
z-index: 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.yd-actor--visible {
|
|
70
|
+
opacity: 1;
|
|
71
|
+
}
|
|
59
72
|
|
|
60
73
|
/* Dialogue box container */
|
|
61
74
|
.yd-dialogue-box {
|
package/src/runtime/results.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { MarkupParseResult } from "../markup/types.js";
|
|
1
2
|
export type TextResult = {
|
|
2
3
|
type: "text";
|
|
3
4
|
text: string;
|
|
4
5
|
speaker?: string;
|
|
5
6
|
tags?: string[];
|
|
7
|
+
markup?: MarkupParseResult;
|
|
6
8
|
nodeCss?: string; // Node-level CSS from &css{} header
|
|
7
9
|
scene?: string; // Scene name from node header
|
|
8
10
|
isDialogueEnd: boolean;
|
|
@@ -10,7 +12,7 @@ export type TextResult = {
|
|
|
10
12
|
|
|
11
13
|
export type OptionsResult = {
|
|
12
14
|
type: "options";
|
|
13
|
-
options: { text: string; tags?: string[]; css?: string }[];
|
|
15
|
+
options: { text: string; tags?: string[]; css?: string; markup?: MarkupParseResult }[];
|
|
14
16
|
nodeCss?: string; // Node-level CSS from &css{} header
|
|
15
17
|
scene?: string; // Scene name from node header
|
|
16
18
|
isDialogueEnd: boolean;
|
package/src/runtime/runner.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IRProgram, IRInstruction, IRNode, IRNodeGroup } from "../compile/ir";
|
|
2
|
+
import type { MarkupParseResult, MarkupSegment, MarkupWrapper } from "../markup/types.js";
|
|
2
3
|
import type { RuntimeResult } from "./results.js";
|
|
3
4
|
import { ExpressionEvaluator } from "./evaluator.js";
|
|
4
5
|
import { CommandHandler, parseCommand } from "./commands.js";
|
|
@@ -205,15 +206,152 @@ export class YarnRunner {
|
|
|
205
206
|
this.step();
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
private interpolate(text: string): string {
|
|
209
|
-
|
|
209
|
+
private interpolate(text: string, markup?: MarkupParseResult): { text: string; markup?: MarkupParseResult } {
|
|
210
|
+
const evaluateExpression = (expr: string): string => {
|
|
210
211
|
try {
|
|
211
|
-
const
|
|
212
|
-
|
|
212
|
+
const value = this.evaluator.evaluateExpression(expr.trim());
|
|
213
|
+
if (value === null || value === undefined) {
|
|
214
|
+
return "";
|
|
215
|
+
}
|
|
216
|
+
return String(value);
|
|
213
217
|
} catch {
|
|
214
218
|
return "";
|
|
215
219
|
}
|
|
216
|
-
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (!markup) {
|
|
223
|
+
const interpolated = text.replace(/\{([^}]+)\}/g, (_m, expr) => evaluateExpression(expr));
|
|
224
|
+
return { text: interpolated };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const segments = markup.segments.filter((segment) => !segment.selfClosing);
|
|
228
|
+
const getWrappersAt = (index: number): MarkupWrapper[] => {
|
|
229
|
+
for (const segment of segments) {
|
|
230
|
+
if (segment.start <= index && index < segment.end) {
|
|
231
|
+
return segment.wrappers.map((wrapper) => ({
|
|
232
|
+
name: wrapper.name,
|
|
233
|
+
type: wrapper.type,
|
|
234
|
+
properties: { ...wrapper.properties },
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (segments.length === 0) {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
if (index > 0) {
|
|
242
|
+
return getWrappersAt(index - 1);
|
|
243
|
+
}
|
|
244
|
+
return segments[0].wrappers.map((wrapper) => ({
|
|
245
|
+
name: wrapper.name,
|
|
246
|
+
type: wrapper.type,
|
|
247
|
+
properties: { ...wrapper.properties },
|
|
248
|
+
}));
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const resultChars: string[] = [];
|
|
252
|
+
const newSegments: MarkupSegment[] = [];
|
|
253
|
+
let currentSegment: MarkupSegment | null = null;
|
|
254
|
+
|
|
255
|
+
const wrappersEqual = (a: MarkupWrapper[], b: MarkupWrapper[]) => {
|
|
256
|
+
if (a.length !== b.length) return false;
|
|
257
|
+
for (let i = 0; i < a.length; i++) {
|
|
258
|
+
const wa = a[i];
|
|
259
|
+
const wb = b[i];
|
|
260
|
+
if (wa.name !== wb.name || wa.type !== wb.type) return false;
|
|
261
|
+
const keysA = Object.keys(wa.properties);
|
|
262
|
+
const keysB = Object.keys(wb.properties);
|
|
263
|
+
if (keysA.length !== keysB.length) return false;
|
|
264
|
+
for (const key of keysA) {
|
|
265
|
+
if (wa.properties[key] !== wb.properties[key]) return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const flushSegment = () => {
|
|
272
|
+
if (currentSegment) {
|
|
273
|
+
newSegments.push(currentSegment);
|
|
274
|
+
currentSegment = null;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const appendCharWithWrappers = (char: string, wrappers: MarkupWrapper[]) => {
|
|
279
|
+
const index = resultChars.length;
|
|
280
|
+
resultChars.push(char);
|
|
281
|
+
const wrappersCopy = wrappers.map((wrapper) => ({
|
|
282
|
+
name: wrapper.name,
|
|
283
|
+
type: wrapper.type,
|
|
284
|
+
properties: { ...wrapper.properties },
|
|
285
|
+
}));
|
|
286
|
+
if (currentSegment && wrappersEqual(currentSegment.wrappers, wrappersCopy)) {
|
|
287
|
+
currentSegment.end = index + 1;
|
|
288
|
+
} else {
|
|
289
|
+
flushSegment();
|
|
290
|
+
currentSegment = { start: index, end: index + 1, wrappers: wrappersCopy };
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const appendStringWithWrappers = (value: string, wrappers: MarkupWrapper[]) => {
|
|
295
|
+
if (!value) {
|
|
296
|
+
flushSegment();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
for (const ch of value) {
|
|
300
|
+
appendCharWithWrappers(ch, wrappers);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
let i = 0;
|
|
305
|
+
while (i < text.length) {
|
|
306
|
+
const char = text[i];
|
|
307
|
+
if (char === '{') {
|
|
308
|
+
const close = text.indexOf('}', i + 1);
|
|
309
|
+
if (close === -1) {
|
|
310
|
+
appendCharWithWrappers(char, getWrappersAt(Math.max(0, Math.min(i, text.length - 1))));
|
|
311
|
+
i += 1;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const expr = text.slice(i + 1, close);
|
|
315
|
+
const evaluated = evaluateExpression(expr);
|
|
316
|
+
const wrappers = getWrappersAt(Math.max(0, Math.min(i, text.length - 1)));
|
|
317
|
+
appendStringWithWrappers(evaluated, wrappers);
|
|
318
|
+
i = close + 1;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
appendCharWithWrappers(char, getWrappersAt(i));
|
|
322
|
+
i += 1;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
flushSegment();
|
|
326
|
+
const interpolatedText = resultChars.join('');
|
|
327
|
+
const normalizedMarkup = this.normalizeMarkupResult({ text: interpolatedText, segments: newSegments });
|
|
328
|
+
return { text: interpolatedText, markup: normalizedMarkup };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private normalizeMarkupResult(result: MarkupParseResult): MarkupParseResult | undefined {
|
|
332
|
+
if (!result) return undefined;
|
|
333
|
+
if (result.segments.length === 0) {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
const hasFormatting = result.segments.some(
|
|
337
|
+
(segment) => segment.wrappers.length > 0 || segment.selfClosing
|
|
338
|
+
);
|
|
339
|
+
if (!hasFormatting) {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
text: result.text,
|
|
344
|
+
segments: result.segments.map((segment) => ({
|
|
345
|
+
start: segment.start,
|
|
346
|
+
end: segment.end,
|
|
347
|
+
wrappers: segment.wrappers.map((wrapper) => ({
|
|
348
|
+
name: wrapper.name,
|
|
349
|
+
type: wrapper.type,
|
|
350
|
+
properties: { ...wrapper.properties },
|
|
351
|
+
})),
|
|
352
|
+
selfClosing: segment.selfClosing,
|
|
353
|
+
})),
|
|
354
|
+
};
|
|
217
355
|
}
|
|
218
356
|
|
|
219
357
|
private resumeBlock(): boolean {
|
|
@@ -229,9 +367,11 @@ export class YarnRunner {
|
|
|
229
367
|
return true;
|
|
230
368
|
}
|
|
231
369
|
switch (ins.op) {
|
|
232
|
-
case "line":
|
|
233
|
-
|
|
370
|
+
case "line": {
|
|
371
|
+
const { text: interpolatedText, markup: interpolatedMarkup } = this.interpolate(ins.text, ins.markup);
|
|
372
|
+
this.emit({ type: "text", text: interpolatedText, speaker: ins.speaker, tags: ins.tags, markup: interpolatedMarkup, isDialogueEnd: false });
|
|
234
373
|
return true;
|
|
374
|
+
}
|
|
235
375
|
case "command": {
|
|
236
376
|
try {
|
|
237
377
|
const parsed = parseCommand(ins.content);
|
|
@@ -244,7 +384,7 @@ export class YarnRunner {
|
|
|
244
384
|
return true;
|
|
245
385
|
}
|
|
246
386
|
case "options": {
|
|
247
|
-
this.emit({ type: "options", options: ins.options.map((o) => ({ text: o.text, tags: o.tags })), isDialogueEnd: false });
|
|
387
|
+
this.emit({ type: "options", options: ins.options.map((o) => ({ text: o.text, tags: o.tags, markup: o.markup })), isDialogueEnd: false });
|
|
248
388
|
return true;
|
|
249
389
|
}
|
|
250
390
|
case "if": {
|
|
@@ -294,9 +434,11 @@ export class YarnRunner {
|
|
|
294
434
|
}
|
|
295
435
|
this.ip++;
|
|
296
436
|
switch (ins.op) {
|
|
297
|
-
case "line":
|
|
298
|
-
|
|
437
|
+
case "line": {
|
|
438
|
+
const { text: interpolatedText, markup: interpolatedMarkup } = this.interpolate(ins.text, ins.markup);
|
|
439
|
+
this.emit({ type: "text", text: interpolatedText, speaker: ins.speaker, tags: ins.tags, markup: interpolatedMarkup, nodeCss: resolved.css, scene: resolved.scene, isDialogueEnd: this.lookaheadIsEnd() });
|
|
299
440
|
return;
|
|
441
|
+
}
|
|
300
442
|
case "command": {
|
|
301
443
|
try {
|
|
302
444
|
const parsed = parseCommand(ins.content);
|
|
@@ -327,7 +469,7 @@ export class YarnRunner {
|
|
|
327
469
|
continue;
|
|
328
470
|
}
|
|
329
471
|
case "options": {
|
|
330
|
-
this.emit({ type: "options", options: ins.options.map((o: { text: string; tags?: string[]; css?: string }) => ({ text: o.text, tags: o.tags, css: o.css })), nodeCss: resolved.css, scene: resolved.scene, isDialogueEnd: this.lookaheadIsEnd() });
|
|
472
|
+
this.emit({ type: "options", options: ins.options.map((o: { text: string; tags?: string[]; css?: string; markup?: MarkupParseResult }) => ({ text: o.text, tags: o.tags, css: o.css, markup: o.markup })), nodeCss: resolved.css, scene: resolved.scene, isDialogueEnd: this.lookaheadIsEnd() });
|
|
331
473
|
return;
|
|
332
474
|
}
|
|
333
475
|
case "if": {
|
|
@@ -368,10 +510,12 @@ export class YarnRunner {
|
|
|
368
510
|
const ins = tempNode.instructions[idx++];
|
|
369
511
|
if (!ins) break;
|
|
370
512
|
switch (ins.op) {
|
|
371
|
-
case "line":
|
|
372
|
-
|
|
513
|
+
case "line": {
|
|
514
|
+
const { text: interpolatedText, markup: interpolatedMarkup } = this.interpolate(ins.text, ins.markup);
|
|
515
|
+
this.emit({ type: "text", text: interpolatedText, speaker: ins.speaker, markup: interpolatedMarkup, isDialogueEnd: false });
|
|
373
516
|
restore();
|
|
374
517
|
return;
|
|
518
|
+
}
|
|
375
519
|
case "command":
|
|
376
520
|
try {
|
|
377
521
|
const parsed = parseCommand(ins.content);
|
|
@@ -384,7 +528,7 @@ export class YarnRunner {
|
|
|
384
528
|
restore();
|
|
385
529
|
return;
|
|
386
530
|
case "options":
|
|
387
|
-
this.emit({ type: "options", options: ins.options.map((o) => ({ text: o.text })), isDialogueEnd: false });
|
|
531
|
+
this.emit({ type: "options", options: ins.options.map((o) => ({ text: o.text, markup: o.markup })), isDialogueEnd: false });
|
|
388
532
|
// Maintain context that options belong to main node at ip-1
|
|
389
533
|
restore();
|
|
390
534
|
return;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import { strictEqual, ok } from "node:assert";
|
|
3
|
+
import { parseMarkup } from "../markup/parser.js";
|
|
4
|
+
import type { MarkupParseResult } from "../markup/types.js";
|
|
5
|
+
|
|
6
|
+
test("parseMarkup handles default HTML tags", () => {
|
|
7
|
+
const result = parseMarkup("This is [b]bold[/b] and [em]emphasized[/em].");
|
|
8
|
+
|
|
9
|
+
strictEqual(result.text, "This is bold and emphasized.");
|
|
10
|
+
const bold = findSegment(result, "bold");
|
|
11
|
+
ok(bold, "Expected bold segment");
|
|
12
|
+
strictEqual(bold.wrappers.length, 1);
|
|
13
|
+
strictEqual(bold.wrappers[0].name, "b");
|
|
14
|
+
strictEqual(bold.wrappers[0].type, "default");
|
|
15
|
+
|
|
16
|
+
const em = findSegment(result, "emphasized");
|
|
17
|
+
ok(em, "Expected em segment");
|
|
18
|
+
strictEqual(em.wrappers[0].name, "em");
|
|
19
|
+
strictEqual(em.wrappers[0].type, "default");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("parseMarkup exposes custom tags with properties", () => {
|
|
23
|
+
const result = parseMarkup("Say [wave speed=2 tone=\"high\"]hello[/wave]!");
|
|
24
|
+
strictEqual(result.text, "Say hello!");
|
|
25
|
+
|
|
26
|
+
const segment = findSegment(result, "hello");
|
|
27
|
+
ok(segment, "Expected wave segment");
|
|
28
|
+
const wrapper = segment.wrappers.find((w) => w.name === "wave");
|
|
29
|
+
ok(wrapper, "Expected wave wrapper");
|
|
30
|
+
strictEqual(wrapper.type, "custom");
|
|
31
|
+
strictEqual(wrapper.properties.speed, 2);
|
|
32
|
+
strictEqual(wrapper.properties.tone, "high");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("parseMarkup handles self-closing tags", () => {
|
|
36
|
+
const result = parseMarkup("[pause length=500/] Ready.");
|
|
37
|
+
strictEqual(result.text, " Ready.");
|
|
38
|
+
ok(
|
|
39
|
+
result.segments.some(
|
|
40
|
+
(seg) => seg.selfClosing && seg.wrappers.some((wrapper) => wrapper.name === "pause")
|
|
41
|
+
),
|
|
42
|
+
"Expected self closing pause marker"
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("parseMarkup respects nomarkup blocks and escaping", () => {
|
|
47
|
+
const result = parseMarkup(`[nomarkup][b] raw [/b][/nomarkup] and \\[escaped\\]`);
|
|
48
|
+
strictEqual(result.text, "[b] raw [/b] and [escaped]");
|
|
49
|
+
ok(
|
|
50
|
+
result.segments.every((segment) => segment.wrappers.length === 0),
|
|
51
|
+
"Expected no wrappers when using nomarkup and escaping"
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function findSegment(result: MarkupParseResult, target: string) {
|
|
56
|
+
return result.segments.find((segment) => {
|
|
57
|
+
if (segment.selfClosing) return false;
|
|
58
|
+
const text = result.text.slice(segment.start, segment.end);
|
|
59
|
+
return text === target;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
|
-
import { strictEqual } from "node:assert";
|
|
2
|
+
import { strictEqual, ok } from "node:assert";
|
|
3
3
|
import { parseYarn, compile, YarnRunner } from "../index.js";
|
|
4
4
|
|
|
5
5
|
test("nodes and lines delivery", () => {
|
|
@@ -25,3 +25,37 @@ Narrator: Line two
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
test("markup parsing propagates to runtime", () => {
|
|
31
|
+
const script = `
|
|
32
|
+
title: Start
|
|
33
|
+
---
|
|
34
|
+
Narrator: Plain [b]bold[/b] [wave speed=2]custom[/wave]
|
|
35
|
+
===
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const doc = parseYarn(script);
|
|
39
|
+
const ir = compile(doc);
|
|
40
|
+
const runner = new YarnRunner(ir, { startAt: "Start" });
|
|
41
|
+
|
|
42
|
+
const result = runner.currentResult;
|
|
43
|
+
ok(result && result.type === "text", "Expected text result with markup");
|
|
44
|
+
ok(result?.markup, "Expected markup data to be present");
|
|
45
|
+
const markup = result!.markup!;
|
|
46
|
+
strictEqual(markup.text, "Plain bold custom");
|
|
47
|
+
|
|
48
|
+
const boldSegment = markup.segments.find((segment) =>
|
|
49
|
+
segment.wrappers.some((wrapper) => wrapper.name === "b" && wrapper.type === "default")
|
|
50
|
+
);
|
|
51
|
+
ok(boldSegment, "Expected bold segment");
|
|
52
|
+
strictEqual(markup.text.slice(boldSegment!.start, boldSegment!.end), "bold");
|
|
53
|
+
|
|
54
|
+
const customSegment = markup.segments.find((segment) =>
|
|
55
|
+
segment.wrappers.some((wrapper) => wrapper.name === "wave" && wrapper.type === "custom")
|
|
56
|
+
);
|
|
57
|
+
ok(customSegment, "Expected custom segment");
|
|
58
|
+
const waveWrapper = customSegment!.wrappers.find((wrapper) => wrapper.name === "wave");
|
|
59
|
+
ok(waveWrapper);
|
|
60
|
+
strictEqual(waveWrapper!.properties.speed, 2);
|
|
61
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
|
-
import { strictEqual } from "node:assert";
|
|
2
|
+
import { strictEqual, ok } from "node:assert";
|
|
3
3
|
import { parseYarn, compile, YarnRunner } from "../index.js";
|
|
4
4
|
|
|
5
5
|
test("options selection", () => {
|
|
@@ -32,3 +32,41 @@ Narrator: Choose one
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
test("option markup is exposed", () => {
|
|
38
|
+
const script = `
|
|
39
|
+
title: Start
|
|
40
|
+
---
|
|
41
|
+
Narrator: Choose
|
|
42
|
+
-> [b]Bold[/b]
|
|
43
|
+
Narrator: Bold
|
|
44
|
+
-> [wave intensity=5]Custom[/wave]
|
|
45
|
+
Narrator: Custom
|
|
46
|
+
===
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const doc = parseYarn(script);
|
|
50
|
+
const ir = compile(doc);
|
|
51
|
+
const runner = new YarnRunner(ir, { startAt: "Start" });
|
|
52
|
+
|
|
53
|
+
runner.advance(); // move to options
|
|
54
|
+
const result = runner.currentResult;
|
|
55
|
+
ok(result && result.type === "options", "Expected options result");
|
|
56
|
+
const options = result!.options;
|
|
57
|
+
ok(options[0].markup, "Expected markup on first option");
|
|
58
|
+
ok(options[1].markup, "Expected markup on second option");
|
|
59
|
+
const boldMarkup = options[0].markup!;
|
|
60
|
+
strictEqual(boldMarkup.text, "Bold");
|
|
61
|
+
ok(
|
|
62
|
+
boldMarkup.segments.some((segment) =>
|
|
63
|
+
segment.wrappers.some((wrapper) => wrapper.name === "b" && wrapper.type === "default")
|
|
64
|
+
),
|
|
65
|
+
"Expected bold wrapper"
|
|
66
|
+
);
|
|
67
|
+
const customWrapper = options[1].markup!.segments
|
|
68
|
+
.flatMap((segment) => segment.wrappers)
|
|
69
|
+
.find((wrapper) => wrapper.name === "wave");
|
|
70
|
+
ok(customWrapper, "Expected custom wrapper on second option");
|
|
71
|
+
strictEqual(customWrapper!.properties.intensity, 5);
|
|
72
|
+
});
|