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
@@ -30,15 +30,15 @@ Narrator: Choose one
30
30
  strictEqual(c.text.includes("Picked B"), true, "Expected body of option B");
31
31
  });
32
32
  test("option markup is exposed", () => {
33
- const script = `
34
- title: Start
35
- ---
36
- Narrator: Choose
37
- -> [b]Bold[/b]
38
- Narrator: Bold
39
- -> [wave intensity=5]Custom[/wave]
40
- Narrator: Custom
41
- ===
33
+ const script = `
34
+ title: Start
35
+ ---
36
+ Narrator: Choose
37
+ -> [b]Bold[/b]
38
+ Narrator: Bold
39
+ -> [wave intensity=5]Custom[/wave]
40
+ Narrator: Custom
41
+ ===
42
42
  `;
43
43
  const doc = parseYarn(script);
44
44
  const ir = compile(doc);
@@ -58,4 +58,159 @@ Narrator: Choose
58
58
  ok(customWrapper, "Expected custom wrapper on second option");
59
59
  strictEqual(customWrapper.properties.intensity, 5);
60
60
  });
61
+ test("option text interpolates variables", () => {
62
+ const script = `
63
+ title: Start
64
+ ---
65
+ <<set $cost to 150>>
66
+ <<set $bribe to 300>>
67
+ Narrator: Decide
68
+ -> Pay {$cost}
69
+ Narrator: Paid
70
+ -> Haggle {$bribe}
71
+ Narrator: Haggle
72
+ ===
73
+ `;
74
+ const doc = parseYarn(script);
75
+ const ir = compile(doc);
76
+ const runner = new YarnRunner(ir, { startAt: "Start" });
77
+ // First result is command for set
78
+ const initial = runner.currentResult;
79
+ strictEqual(initial?.type, "command", "Expected first <<set>> to emit a command result");
80
+ runner.advance(); // second <<set>> command
81
+ runner.advance(); // move to narration
82
+ runner.advance(); // move to options
83
+ const result = runner.currentResult;
84
+ if (!result || result.type !== "options") {
85
+ throw new Error("Expected to land on options");
86
+ }
87
+ const [pay, haggle] = result.options;
88
+ strictEqual(pay.text, "Pay 150", "Should replace placeholder with variable value");
89
+ strictEqual(haggle.text, "Haggle 300", "Should evaluate expressions inside placeholders");
90
+ });
91
+ test("conditional options respect once blocks and if statements", () => {
92
+ const script = `
93
+ title: Start
94
+ ---
95
+ <<declare $secret = false>>
96
+ Narrator: Boot
97
+ <<once>>
98
+ <<set $secret = true>>
99
+ <<endonce>>
100
+ Narrator: Menu
101
+ <<if $secret>>
102
+ -> Secret Option
103
+ Narrator: Secret taken
104
+ <<set $secret = false>>
105
+ <<jump Start>>
106
+ <<endif>>
107
+ -> Regular Option
108
+ Narrator: Regular taken
109
+ <<jump Start>>
110
+ ===
111
+ `;
112
+ const doc = parseYarn(script);
113
+ const ir = compile(doc);
114
+ const runner = new YarnRunner(ir, { startAt: "Start" });
115
+ const nextOptions = () => {
116
+ let guard = 25;
117
+ while (guard-- > 0) {
118
+ const result = runner.currentResult;
119
+ if (!result)
120
+ throw new Error("Expected runtime result");
121
+ if (result.type === "options") {
122
+ return result;
123
+ }
124
+ runner.advance();
125
+ }
126
+ throw new Error("Failed to reach options result");
127
+ };
128
+ const secretMenu = nextOptions();
129
+ strictEqual(secretMenu.options.length, 1, "First pass should expose the conditional secret option");
130
+ strictEqual(secretMenu.options[0].text, "Secret Option");
131
+ // Consume the secret option to flip the flag off
132
+ runner.advance(0);
133
+ const fallbackMenu = nextOptions();
134
+ strictEqual(fallbackMenu.options.length, 1, "After the secret path is used, only the regular option should remain");
135
+ strictEqual(fallbackMenu.options[0].text, "Regular Option");
136
+ });
137
+ test("options allow space-indented bodies", () => {
138
+ const script = `
139
+ title: Start
140
+ ---
141
+ -> Pay
142
+ <<jump Pay>>
143
+ -> Run
144
+ <<jump Run>>
145
+ ===
146
+
147
+ title: Pay
148
+ ---
149
+ Narrator: Pay branch
150
+ ===
151
+
152
+ title: Run
153
+ ---
154
+ Narrator: Run branch
155
+ ===
156
+ `;
157
+ const doc = parseYarn(script);
158
+ const ir = compile(doc);
159
+ const runner = new YarnRunner(ir, { startAt: "Start" });
160
+ const initial = runner.currentResult;
161
+ if (initial?.type !== "options") {
162
+ runner.advance();
163
+ }
164
+ const optionsResult = runner.currentResult;
165
+ strictEqual(optionsResult?.type, "options", "Expected to reach options");
166
+ if (optionsResult?.type !== "options")
167
+ throw new Error("Options not emitted");
168
+ strictEqual(optionsResult.options.length, 2, "Space indents should still group options together");
169
+ strictEqual(optionsResult.options[0].text, "Pay");
170
+ strictEqual(optionsResult.options[1].text, "Run");
171
+ });
172
+ test("inline [if] option condition filters options", () => {
173
+ const script = `
174
+ title: StartFalse
175
+ ---
176
+ <<declare $flag = false>>
177
+ -> Hidden [if $flag]
178
+ Narrator: Hidden
179
+ -> Visible
180
+ Narrator: Visible
181
+ ===
182
+
183
+ title: StartTrue
184
+ ---
185
+ <<declare $flag = true>>
186
+ -> Hidden [if $flag]
187
+ Narrator: Hidden
188
+ -> Visible
189
+ Narrator: Visible
190
+ ===
191
+ `;
192
+ const doc = parseYarn(script);
193
+ const ir = compile(doc);
194
+ const getOptions = (startNode) => {
195
+ const runner = new YarnRunner(ir, { startAt: startNode });
196
+ let guard = 25;
197
+ while (guard-- > 0) {
198
+ const result = runner.currentResult;
199
+ if (!result)
200
+ break;
201
+ if (result.type === "options") {
202
+ return { runner, options: result };
203
+ }
204
+ runner.advance();
205
+ }
206
+ throw new Error("Failed to reach options");
207
+ };
208
+ const { options: optionsFalse } = getOptions("StartFalse");
209
+ strictEqual(optionsFalse.options.length, 1, "Hidden option should be filtered out when condition is false");
210
+ strictEqual(optionsFalse.options[0].text, "Visible");
211
+ const { options: optionsTrue } = getOptions("StartTrue");
212
+ strictEqual(optionsTrue.options.length, 2, "Both options should appear when condition is true");
213
+ strictEqual(optionsTrue.options[0].text, "Hidden");
214
+ strictEqual(optionsTrue.options[1].text, "Visible");
215
+ });
61
216
  //# sourceMappingURL=options.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"options.test.js","sourceRoot":"","sources":["../../src/tests/options.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"options.test.js","sourceRoot":"","sources":["../../src/tests/options.test.ts"],"names":[],"mappings":"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"}
@@ -2,16 +2,16 @@ import { test } from "node:test";
2
2
  import { strictEqual } from "node:assert";
3
3
  import { parseYarn, compile, YarnRunner } from "../index.js";
4
4
  test("variables, flow control, and commands", () => {
5
- const script = `
6
- title: Start
7
- ---
8
- <<set $score to 10>>
9
- <<if $score >= 10>>
10
- Narrator: High
11
- <<else>>
12
- Narrator: Low
13
- <<endif>>
14
- ===
5
+ const script = `
6
+ title: Start
7
+ ---
8
+ <<set $score to 10>>
9
+ <<if $score >= 10>>
10
+ Narrator: High
11
+ <<else>>
12
+ Narrator: Low
13
+ <<endif>>
14
+ ===
15
15
  `;
16
16
  const doc = parseYarn(script);
17
17
  const ir = compile(doc);
@@ -27,4 +27,46 @@ title: Start
27
27
  strictEqual(/High/.test(b.text), true, "Expected High branch");
28
28
  strictEqual(runner.getVariable("score"), 10, "Variable should be set");
29
29
  });
30
+ test("equality operators support ==, !=, and single =", () => {
31
+ const script = `
32
+ title: Start
33
+ ---
34
+ <<set $doorOpen to true>>
35
+ <<if $doorOpen = true>>
36
+ Narrator: Single equals ok
37
+ <<endif>>
38
+ <<if $doorOpen == true>>
39
+ Narrator: Double equals ok
40
+ <<endif>>
41
+ <<if $doorOpen != false>>
42
+ Narrator: Not equals ok
43
+ <<endif>>
44
+ ===
45
+ `;
46
+ const doc = parseYarn(script);
47
+ const ir = compile(doc);
48
+ const runner = new YarnRunner(ir, { startAt: "Start" });
49
+ const seen = [];
50
+ let guard = 25;
51
+ while (guard-- > 0) {
52
+ const result = runner.currentResult;
53
+ if (!result)
54
+ break;
55
+ if (result.type === "text" && result.text.trim()) {
56
+ seen.push(result.text.trim());
57
+ }
58
+ if (result.isDialogueEnd) {
59
+ break;
60
+ }
61
+ if (result.type === "options") {
62
+ runner.advance(0);
63
+ }
64
+ else {
65
+ runner.advance();
66
+ }
67
+ }
68
+ strictEqual(seen.includes("Single equals ok"), true, "Single equals comparison should succeed");
69
+ strictEqual(seen.includes("Double equals ok"), true, "Double equals comparison should succeed");
70
+ strictEqual(seen.includes("Not equals ok"), true, "Not equals comparison should succeed");
71
+ });
30
72
  //# sourceMappingURL=variables_flow_cmds.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"variables_flow_cmds.test.js","sourceRoot":"","sources":["../../src/tests/variables_flow_cmds.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE7D,IAAI,CAAC,uCAAuC,EAAE,GAAG,EAAE;IACjD,MAAM,MAAM,GAAG;;;;;;;;;;CAUhB,CAAC;IAEA,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IAC9B,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACxB,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IAExD,yCAAyC;IACzC,0CAA0C;IAC1C,MAAM,CAAC,GAAG,MAAM,CAAC,aAAc,CAAC;IAChC,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,gCAAgC,CAAC,CAAC;IACjE,MAAM,CAAC,OAAO,EAAE,CAAC;IACjB,MAAM,CAAC,GAAG,MAAM,CAAC,aAAc,CAAC;IAChC,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,8BAA8B,CAAC,CAAC;IAC5D,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM;QAAE,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,sBAAsB,CAAC,CAAC;IACtF,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,wBAAwB,CAAC,CAAC;AACzE,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"variables_flow_cmds.test.js","sourceRoot":"","sources":["../../src/tests/variables_flow_cmds.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE7D,IAAI,CAAC,uCAAuC,EAAE,GAAG,EAAE;IACjD,MAAM,MAAM,GAAG;;;;;;;;;;CAUhB,CAAC;IAEA,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IAC9B,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACxB,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IAExD,yCAAyC;IACzC,0CAA0C;IAC1C,MAAM,CAAC,GAAG,MAAM,CAAC,aAAc,CAAC;IAChC,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,gCAAgC,CAAC,CAAC;IACjE,MAAM,CAAC,OAAO,EAAE,CAAC;IACjB,MAAM,CAAC,GAAG,MAAM,CAAC,aAAc,CAAC;IAChC,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,8BAA8B,CAAC,CAAC;IAC5D,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM;QAAE,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,sBAAsB,CAAC,CAAC;IACtF,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,wBAAwB,CAAC,CAAC;AACzE,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,iDAAiD,EAAE,GAAG,EAAE;IAC3D,MAAM,MAAM,GAAG;;;;;;;;;;;;;;CAchB,CAAC;IAEA,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IAC9B,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACxB,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IAExD,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,OAAO,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;QACnB,MAAM,MAAM,GAAG,MAAM,CAAC,aAAa,CAAC;QACpC,IAAI,CAAC,MAAM;YAAE,MAAM;QACnB,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YACjD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAChC,CAAC;QACD,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;YACzB,MAAM;QACR,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC9B,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QACpB,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,CAAC;IACH,CAAC;IAED,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,IAAI,EAAE,yCAAyC,CAAC,CAAC;IAChG,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,IAAI,EAAE,yCAAyC,CAAC,CAAC;IAChG,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,IAAI,EAAE,sCAAsC,CAAC,CAAC;AAC5F,CAAC,CAAC,CAAC"}
package/docs/markup.md CHANGED
@@ -1,34 +1,34 @@
1
- ## Markup (Yarn Spinner)
2
-
3
- Source: [docs.yarnspinner.dev � Markup](https://docs.yarnspinner.dev/write-yarn-scripts/advanced-scripting/markup)
4
-
5
- ### Supported formatting
6
-
7
- The runtime now parses Yarn Spinner markup and surfaces it through `TextResult.markup` and option metadata. The React components (`DialogueView`, `TypingText`, and the option buttons) render this markup automatically.
8
-
9
- - The following tags map directly to native HTML elements: `b`, `strong`, `em`, `small`, `sub`, `sup`, `ins`, `del`, and `mark`.
10
- - Any other markup tag is rendered as a `<span>` with the class `yd-markup-<tagName>` so you can style or animate it via CSS.
11
- - Markup attributes are exposed as `data-markup-*` attributes on the rendered element. For example `[wave speed=2]` renders `<span class="yd-markup-wave" data-markup-speed="2">`.
12
-
13
- ### Example
14
-
15
- ```yarn
16
- title: Start
17
- ---
18
- Narrator: Plain [b]bold[/b] [wave speed=2]custom[/wave]
19
- ===
20
- ```
21
-
22
- The React renderer produces:
23
-
24
- ```html
25
- Plain <b>bold</b> <span class="yd-markup-wave" data-markup-speed="2">custom</span>
26
- ```
27
-
28
- ### Integration notes
29
-
30
- - Markup data is available on `TextResult.markup` and on each option entry (`result.options[i].markup`).
31
- - `TypingText` respects markup while animating, so formatting stays intact during the typewriter effect.
32
- - When a markup tag is not recognised, it remains in the output (as a span) rather than being stripped, so you can add custom CSS in your host application.
33
-
1
+ ## Markup (Yarn Spinner)
2
+
3
+ Source: [docs.yarnspinner.dev � Markup](https://docs.yarnspinner.dev/write-yarn-scripts/advanced-scripting/markup)
4
+
5
+ ### Supported formatting
6
+
7
+ The runtime now parses Yarn Spinner markup and surfaces it through `TextResult.markup` and option metadata. The React components (`DialogueView`, `TypingText`, and the option buttons) render this markup automatically.
8
+
9
+ - The following tags map directly to native HTML elements: `b`, `strong`, `em`, `small`, `sub`, `sup`, `ins`, `del`, `mark`, and the self-closing line break `br`.
10
+ - Any other markup tag is rendered as a `<span>` with the class `yd-markup-<tagName>` so you can style or animate it via CSS.
11
+ - Markup attributes are exposed as `data-markup-*` attributes on the rendered element. For example `[wave speed=2]` renders `<span class="yd-markup-wave" data-markup-speed="2">`.
12
+
13
+ ### Example
14
+
15
+ ```yarn
16
+ title: Start
17
+ ---
18
+ Narrator: Plain [b]bold[/b] [wave speed=2]custom[/wave]
19
+ ===
20
+ ```
21
+
22
+ The React renderer produces:
23
+
24
+ ```html
25
+ Plain <b>bold</b> <span class="yd-markup-wave" data-markup-speed="2">custom</span>
26
+ ```
27
+
28
+ ### Integration notes
29
+
30
+ - Markup data is available on `TextResult.markup` and on each option entry (`result.options[i].markup`).
31
+ - `TypingText` respects markup while animating, so formatting stays intact during the typewriter effect.
32
+ - When a markup tag is not recognised, it remains in the output (as a span) rather than being stripped, so you can add custom CSS in your host application.
33
+
34
34
  For the full markup vocabulary see the official Yarn Spinner documentation.
package/eslint.config.cjs CHANGED
@@ -1,39 +1,39 @@
1
- // ESLint v9 flat config
2
- const js = require("@eslint/js");
3
- const tsParser = require("@typescript-eslint/parser");
4
- const tsPlugin = require("@typescript-eslint/eslint-plugin");
5
-
6
- module.exports = [
7
- {
8
- ignores: ["dist/**", "node_modules/**", "examples/**", "src/examples/**", "src/tests/**"],
9
- },
10
- {
11
- files: ["src/**/*.ts", "src/**/*.tsx"],
12
- languageOptions: {
13
- parser: tsParser,
14
- ecmaVersion: "latest",
15
- sourceType: "module",
16
- globals: {
17
- console: "readonly",
18
- window: "readonly",
19
- setTimeout: "readonly",
20
- clearTimeout: "readonly",
21
- setInterval: "readonly",
22
- clearInterval: "readonly",
23
- requestAnimationFrame: "readonly",
24
- },
25
- },
26
- plugins: {
27
- "@typescript-eslint": tsPlugin,
28
- },
29
- rules: {
30
- ...js.configs.recommended.rules,
31
- ...tsPlugin.configs.recommended.rules,
32
- "@typescript-eslint/explicit-module-boundary-types": "off",
33
- "@typescript-eslint/no-explicit-any": "off",
34
- "no-console": ["warn", { allow: ["warn", "error", "assert", "log"] }],
35
- },
36
- },
37
- ];
38
-
39
-
1
+ // ESLint v9 flat config
2
+ const js = require("@eslint/js");
3
+ const tsParser = require("@typescript-eslint/parser");
4
+ const tsPlugin = require("@typescript-eslint/eslint-plugin");
5
+
6
+ module.exports = [
7
+ {
8
+ ignores: ["dist/**", "node_modules/**", "examples/**", "src/examples/**", "src/tests/**"],
9
+ },
10
+ {
11
+ files: ["src/**/*.ts", "src/**/*.tsx"],
12
+ languageOptions: {
13
+ parser: tsParser,
14
+ ecmaVersion: "latest",
15
+ sourceType: "module",
16
+ globals: {
17
+ console: "readonly",
18
+ window: "readonly",
19
+ setTimeout: "readonly",
20
+ clearTimeout: "readonly",
21
+ setInterval: "readonly",
22
+ clearInterval: "readonly",
23
+ requestAnimationFrame: "readonly",
24
+ },
25
+ },
26
+ plugins: {
27
+ "@typescript-eslint": tsPlugin,
28
+ },
29
+ rules: {
30
+ ...js.configs.recommended.rules,
31
+ ...tsPlugin.configs.recommended.rules,
32
+ "@typescript-eslint/explicit-module-boundary-types": "off",
33
+ "@typescript-eslint/no-explicit-any": "off",
34
+ "no-console": ["warn", { allow: ["warn", "error", "assert", "log"] }],
35
+ },
36
+ },
37
+ ];
38
+
39
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yarn-spinner-runner-ts",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "description": "TypeScript parser, compiler, and runtime for Yarn Spinner 3.x with React adapter [NPM package](https://www.npmjs.com/package/yarn-spinner-runner-ts)",
6
6
  "license": "MIT",
@@ -37,20 +37,20 @@
37
37
  "js-yaml": "^4.1.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@eslint/js": "^9.12.0",
41
- "@types/js-yaml": "^4.0.9",
42
40
  "@types/node": "^22.7.4",
41
+ "@types/js-yaml": "^4.0.9",
43
42
  "@types/react": "^18.3.3",
44
43
  "@types/react-dom": "^18.3.0",
45
- "@typescript-eslint/eslint-plugin": "^8.8.1",
44
+ "@eslint/js": "^9.12.0",
46
45
  "@typescript-eslint/parser": "^8.8.1",
46
+ "@typescript-eslint/eslint-plugin": "^8.8.1",
47
47
  "@vitejs/plugin-react": "^4.3.1",
48
48
  "eslint": "^9.12.0",
49
49
  "react": "^18.3.1",
50
50
  "react-dom": "^18.3.1",
51
51
  "rimraf": "^6.0.1",
52
52
  "typescript": "^5.6.3",
53
- "vite": "^5.4.3",
54
- "vitest": "^4.0.7"
53
+ "vite": "^5.4.3"
55
54
  }
56
55
  }
56
+
@@ -72,7 +72,7 @@ export function compile(doc: YarnDocument, opts: CompileOptions = {}): IRProgram
72
72
  }
73
73
  block.push({
74
74
  op: "options",
75
- options: s.options.map((o: Option) => ({ text: o.text, tags: ensureLineId(o.tags), css: (o as any).css, markup: o.markup, block: emitBlock(o.body) })),
75
+ options: s.options.map((o: Option) => ({ text: o.text, tags: ensureLineId(o.tags), css: (o as any).css, markup: o.markup, condition: o.condition, block: emitBlock(o.body) })),
76
76
  });
77
77
  break;
78
78
  }
@@ -141,7 +141,7 @@ export function compile(doc: YarnDocument, opts: CompileOptions = {}): IRProgram
141
141
  }
142
142
  block.push({
143
143
  op: "options",
144
- options: s.options.map((o: Option) => ({ text: o.text, tags: ensureLineId(o.tags), css: (o as any).css, markup: o.markup, block: emitBlock(o.body) })),
144
+ options: s.options.map((o: Option) => ({ text: o.text, tags: ensureLineId(o.tags), css: (o as any).css, markup: o.markup, condition: o.condition, block: emitBlock(o.body) })),
145
145
  });
146
146
  break;
147
147
  }
package/src/compile/ir.ts CHANGED
@@ -22,7 +22,7 @@ export type IRInstruction =
22
22
  | { op: "command"; content: string }
23
23
  | { op: "jump"; target: string }
24
24
  | { op: "detour"; target: string }
25
- | { op: "options"; options: Array<{ text: string; tags?: string[]; css?: string; markup?: MarkupParseResult; block: IRInstruction[] }> }
25
+ | { op: "options"; options: Array<{ text: string; tags?: string[]; css?: string; markup?: MarkupParseResult; condition?: string; block: IRInstruction[] }> }
26
26
  | { op: "if"; branches: Array<{ condition: string | null; block: IRInstruction[] }> }
27
27
  | { op: "once"; id: string; block: IRInstruction[] };
28
28
 
@@ -1,6 +1,7 @@
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"]);
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", "br"]);
4
+ const SELF_CLOSING_TAGS = new Set(["br"]);
4
5
 
5
6
  interface StackEntry {
6
7
  name: string;
@@ -125,8 +126,9 @@ export function parseMarkup(input: string): MarkupParseResult {
125
126
  rest = rest.slice(attrMatch[0].length).trim();
126
127
  }
127
128
 
128
- return { kind, name, properties };
129
- };
129
+ const finalKind: ParsedTag["kind"] = kind === "self" || SELF_CLOSING_TAGS.has(name) ? "self" : kind;
130
+ return { kind: finalKind, name, properties };
131
+ };
130
132
 
131
133
  const parseAttributeValue = (raw: string): MarkupValue => {
132
134
  const trimmed = raw.trim();
@@ -142,20 +144,20 @@ export function parseMarkup(input: string): MarkupParseResult {
142
144
  return trimmed;
143
145
  };
144
146
 
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
- };
147
+ const handleSelfClosing = (tag: ParsedTag) => {
148
+ const wrapper: MarkupWrapper = {
149
+ name: tag.name,
150
+ type: DEFAULT_HTML_TAGS.has(tag.name) ? "default" : "custom",
151
+ properties: tag.properties,
152
+ };
153
+ const position = chars.length;
154
+ pushSegment({
155
+ start: position,
156
+ end: position,
157
+ wrappers: [wrapper],
158
+ selfClosing: true,
159
+ });
160
+ };
159
161
 
160
162
  let i = 0;
161
163
  while (i < input.length) {
@@ -215,30 +217,38 @@ export function parseMarkup(input: string): MarkupParseResult {
215
217
  continue;
216
218
  }
217
219
 
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
- }
220
+ if (parsed.kind === "self") {
221
+ handleSelfClosing(parsed);
222
+ i = closeIndex + 1;
223
+ continue;
224
+ }
225
+
226
+ // closing tag
227
+ if (stack.length === 0) {
228
+ if (SELF_CLOSING_TAGS.has(parsed.name)) {
229
+ i = closeIndex + 1;
230
+ continue;
231
+ }
232
+ appendLiteral(originalText);
233
+ i = closeIndex + 1;
234
+ continue;
235
+ }
236
+ const top = stack[stack.length - 1];
237
+ if (top.name === parsed.name) {
238
+ flushCurrentSegment();
239
+ stack.pop();
240
+ i = closeIndex + 1;
241
+ continue;
242
+ }
243
+ if (SELF_CLOSING_TAGS.has(parsed.name)) {
244
+ i = closeIndex + 1;
245
+ continue;
246
+ }
247
+ // mismatched closing; treat as literal
248
+ appendLiteral(originalText);
249
+ i = closeIndex + 1;
250
+ continue;
251
+ }
242
252
 
243
253
  appendChar(char);
244
254
  i += 1;
package/src/model/ast.ts CHANGED
@@ -73,6 +73,7 @@ export interface Option {
73
73
  tags?: string[];
74
74
  css?: string; // Custom CSS style for option
75
75
  markup?: MarkupParseResult;
76
+ condition?: string;
76
77
  }
77
78
 
78
79
  export interface IfBlock {