@elvishscout/mdstory 0.1.4 → 0.2.0
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/LICENSE +21 -21
- package/README.md +323 -438
- package/README.zh-CN.md +323 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/cli/commands/build.js +33 -0
- package/dist/cli/commands/play.js +9 -0
- package/dist/cli/index.js +44 -0
- package/dist/cli/markdown.js +27 -0
- package/dist/cli/prompt.js +44 -0
- package/dist/core/chapter.js +31 -0
- package/dist/core/definitions.js +2 -0
- package/dist/{base → core}/index.js +2 -1
- package/dist/core/parser.js +228 -0
- package/dist/{base/chapter.js → core/render.js} +45 -63
- package/dist/core/scene.js +28 -0
- package/dist/core/schema.js +43 -0
- package/dist/core/story.js +247 -0
- package/dist/core/utils.js +66 -0
- package/dist/index.js +1 -7
- package/dist/tools/count-words.js +22 -0
- package/html-template/dist/index.html +73 -0
- package/package.json +30 -10
- package/types/cli/commands/build.d.ts +6 -0
- package/types/cli/commands/play.d.ts +4 -0
- package/types/cli/index.d.ts +2 -0
- package/types/cli/markdown.d.ts +2 -0
- package/types/cli/prompt.d.ts +3 -0
- package/types/core/chapter.d.ts +19 -0
- package/types/core/definitions.d.ts +83 -0
- package/types/{base → core}/index.d.ts +2 -1
- package/types/core/parser.d.ts +39 -0
- package/types/core/render.d.ts +46 -0
- package/types/core/scene.d.ts +20 -0
- package/types/core/schema.d.ts +92 -0
- package/types/core/story.d.ts +54 -0
- package/types/core/utils.d.ts +7 -0
- package/types/index.d.ts +1 -5
- package/types/tools/count-words.d.ts +1 -0
- package/dist/base/definitions.js +0 -29
- package/dist/base/error.js +0 -30
- package/dist/base/parser.js +0 -86
- package/dist/base/story.js +0 -82
- package/types/base/chapter.d.ts +0 -50
- package/types/base/definitions.d.ts +0 -113
- package/types/base/error.d.ts +0 -20
- package/types/base/parser.d.ts +0 -2
- package/types/base/story.d.ts +0 -19
package/README.md
CHANGED
|
@@ -1,438 +1,323 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
- `
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
###
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
Defines a chapter.
|
|
327
|
-
|
|
328
|
-
#### Properties
|
|
329
|
-
|
|
330
|
-
- `id`: The unique identifier of the chapter.
|
|
331
|
-
- `title`: The title of the chapter.
|
|
332
|
-
- `template`: The Handlebars template for rendering.
|
|
333
|
-
- `hooks`: Chapter hooks of type [ChapterHooks](#type-chapterhooks).
|
|
334
|
-
|
|
335
|
-
#### Methods
|
|
336
|
-
|
|
337
|
-
- `constructor`: Initializes the chapter instance.
|
|
338
|
-
- Parameters:
|
|
339
|
-
- `options`: An object of type [ChapterOptions](#type-chapteroptions).
|
|
340
|
-
- `render`: Renders the chapter content.
|
|
341
|
-
- Parameters:
|
|
342
|
-
- `scope`: An object of type [Scope](#type-scope) for template rendering.
|
|
343
|
-
- `assets`: An object of [Asset](#type-asset) objects by their names.
|
|
344
|
-
- `options`: An object of type [RenderOptions](#type-renderoptions).
|
|
345
|
-
- Return value:
|
|
346
|
-
- An object of type [RenderResult](#type-renderresult).
|
|
347
|
-
|
|
348
|
-
### `type StoryPrompt`
|
|
349
|
-
|
|
350
|
-
```typescript
|
|
351
|
-
type StoryPrompt = (props: { chapter: Chapter } & RenderResult) => Promise<{ target: string | null; updates: Scope } | FormData>;
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
Defines the prompt function of the story, used to handle user input.
|
|
355
|
-
|
|
356
|
-
#### Parameters
|
|
357
|
-
|
|
358
|
-
- `props`: An object containing the current chapter and rendering result.
|
|
359
|
-
- `chapter`: Current [Chapter](#class-chapter).
|
|
360
|
-
- `text`: The rendered chapter content.
|
|
361
|
-
- `inputs`, `navs`: Fields from [RenderResult](#type-renderresult).
|
|
362
|
-
|
|
363
|
-
#### Return value
|
|
364
|
-
|
|
365
|
-
- A `Promise` resolving to one of the following forms:
|
|
366
|
-
- `{ target, updates }`: The `id` of the target chapter and updated [Scope](#type-scope) of global variables.
|
|
367
|
-
- `FormData`: Contains the form data of user input, typically from a Web app.
|
|
368
|
-
|
|
369
|
-
### `class StoryBase`
|
|
370
|
-
|
|
371
|
-
```typescript
|
|
372
|
-
class StoryBase {
|
|
373
|
-
metadata: Metadata;
|
|
374
|
-
globals: Scope;
|
|
375
|
-
chapters: Record<string, Chapter>;
|
|
376
|
-
entry: Chapter | null;
|
|
377
|
-
hooks: StoryHooks;
|
|
378
|
-
stylesheet: string;
|
|
379
|
-
assets: Record<string, Asset>;
|
|
380
|
-
|
|
381
|
-
constructor(storyBody: StoryBody);
|
|
382
|
-
play(prompt: StoryPrompt, options: RenderOptions): Promise<void>;
|
|
383
|
-
}
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
Defines the base class of the story, containing the core logic of the story.
|
|
387
|
-
|
|
388
|
-
#### Properties
|
|
389
|
-
|
|
390
|
-
- `metadata`: Story [Metadata](#type-metadata).
|
|
391
|
-
- `globals`: A [Scope](#type-scope) of global variables.
|
|
392
|
-
- `chapters`: An object of [Chapter](#class-chapter) objects by their `id`s.
|
|
393
|
-
- `entry`: The entry [Chapter](#class-chapter) of the story.
|
|
394
|
-
- `hooks`: An object of type [StoryHooks](#type-storyhooks).
|
|
395
|
-
- `stylesheet`: The global stylesheet of the story.
|
|
396
|
-
- `assets`: An object of [Asset](#type-asset) objects by their names.
|
|
397
|
-
|
|
398
|
-
#### Methods
|
|
399
|
-
|
|
400
|
-
- `constructor`: Initializes the story instance.
|
|
401
|
-
- `play`: Starts playing the story.
|
|
402
|
-
- Parameters:
|
|
403
|
-
- `prompt`: A function of type [StoryPrompt](#type-storyprompt).
|
|
404
|
-
- `options`: An object of type [RenderOptions](#type-renderoptions).
|
|
405
|
-
- Return value:
|
|
406
|
-
- A `Promise` resolving to `void`.
|
|
407
|
-
|
|
408
|
-
### `function parseStorySource`
|
|
409
|
-
|
|
410
|
-
```typescript
|
|
411
|
-
function parseStorySource(source: string): StoryBody;
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
Parses the story source in Markdown format.
|
|
415
|
-
|
|
416
|
-
#### Parameters
|
|
417
|
-
|
|
418
|
-
- `source`: The story source in Markdown format.
|
|
419
|
-
|
|
420
|
-
#### Return value
|
|
421
|
-
|
|
422
|
-
- An object of type [StoryBody](#type-storybody).
|
|
423
|
-
|
|
424
|
-
### `class Story`
|
|
425
|
-
|
|
426
|
-
```typescript
|
|
427
|
-
class Story extends StoryBase {
|
|
428
|
-
constructor(source: string);
|
|
429
|
-
}
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
Inherits from [StoryBase](#class-storybase), creating a story instance from the story source in Markdown format.
|
|
433
|
-
|
|
434
|
-
#### Methods
|
|
435
|
-
|
|
436
|
-
- `constructor`: Initializes the story instance.
|
|
437
|
-
- Parameters:
|
|
438
|
-
- `source`: The story source in Markdown format.
|
|
1
|
+
**English** | [中文](README.zh-CN.md) | [Writing Guide](WRITING_GUIDE.zh-CN.md)
|
|
2
|
+
|
|
3
|
+
# MdStory
|
|
4
|
+
|
|
5
|
+
An interactive fiction scripting format based on Markdown and Handlebars.
|
|
6
|
+
|
|
7
|
+
Online demo: <https://mdstory.elvish.cc>
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
A MdStory file is a Markdown document with three levels of headings:
|
|
12
|
+
|
|
13
|
+
```markdown
|
|
14
|
+
---
|
|
15
|
+
title: My Story
|
|
16
|
+
globals:
|
|
17
|
+
name: Alice
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# My Story
|
|
21
|
+
|
|
22
|
+
<script>
|
|
23
|
+
export default {
|
|
24
|
+
globals() {
|
|
25
|
+
return { gold: 100 };
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
## Chapter One {#chap1}
|
|
31
|
+
|
|
32
|
+
### A Dark Forest {#forest}
|
|
33
|
+
|
|
34
|
+
You wake up in a dark forest. Your name is {{name}} and you have {{gold}} gold.
|
|
35
|
+
|
|
36
|
+
{{input "string" $weapon="stick"}}
|
|
37
|
+
|
|
38
|
+
{{#nav "chap2.cave"}}Walk forward{{/nav}}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Structure
|
|
42
|
+
|
|
43
|
+
| Level | Heading | Purpose |
|
|
44
|
+
| ----- | ----------- | ---------------------------------------------------------------- |
|
|
45
|
+
| `#` | Story title | Optional. `<script>` before chapters/scenes exports story hooks. |
|
|
46
|
+
| `##` | Chapter | Groups scenes. Has its own hooks and `locals`. |
|
|
47
|
+
| `###` | Scene | Renderable unit with a Handlebars template. |
|
|
48
|
+
|
|
49
|
+
If no `#` heading is present, the story title comes from metadata `title`, or is empty.
|
|
50
|
+
Scenes placed before any `##` are grouped into an implicit default chapter.
|
|
51
|
+
|
|
52
|
+
Content between the `#` heading and the first `##`/`###` is the **story template** — it is rendered once at the beginning of the story. Content between a `##` heading and its first `###` is the **chapter template** — it is rendered once when entering that chapter. Both support the same Handlebars syntax and helpers as scenes.
|
|
53
|
+
|
|
54
|
+
```markdown
|
|
55
|
+
# The Dungeon
|
|
56
|
+
|
|
57
|
+
*You open a dusty tome...*
|
|
58
|
+
|
|
59
|
+
## Chapter One {#ch1}
|
|
60
|
+
|
|
61
|
+
*The air grows cold as you descend.*
|
|
62
|
+
|
|
63
|
+
### The Entrance {#entrance}
|
|
64
|
+
|
|
65
|
+
You stand before a massive iron door.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Navigation
|
|
69
|
+
|
|
70
|
+
Use `{{#nav target}}label{{/nav}}` to let the reader move between scenes:
|
|
71
|
+
|
|
72
|
+
```markdown
|
|
73
|
+
{{#nav "forest"}} Go back to the forest {{/nav}} ← same chapter
|
|
74
|
+
{{#nav "chap2.cave"}} Enter the cave (other chapter){{/nav}} ← cross-chapter
|
|
75
|
+
{{#nav "chap2"}} Go to chapter 2 {{/nav}} ← chapter entry scene
|
|
76
|
+
{{#nav null}} The end {{/nav}} ← end story
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Input
|
|
80
|
+
|
|
81
|
+
Let the reader provide values. `input` does not pause the story where it appears; when the reader leaves the current scene, all inputs in that scene are submitted together with the selected navigation target.
|
|
82
|
+
|
|
83
|
+
Inputs write to chapter `locals` by default. Prefix the variable name with `$` to write to `globals`.
|
|
84
|
+
|
|
85
|
+
```markdown
|
|
86
|
+
{{input "string" name="Alice"}} ← local text input
|
|
87
|
+
{{input "number" age=30}} ← local number input
|
|
88
|
+
{{input "boolean" brave=true}} ← local checkbox
|
|
89
|
+
{{input "string" $name="Alice"}} ← global text input
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Use global values later anywhere in the story:
|
|
93
|
+
|
|
94
|
+
```markdown
|
|
95
|
+
Your name is {{name}}.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Logic & Variables
|
|
99
|
+
|
|
100
|
+
Handlebars `{{#if}}` works with boolean globals and locals:
|
|
101
|
+
|
|
102
|
+
```markdown
|
|
103
|
+
{{#if hasKey}}
|
|
104
|
+
You unlock the door.
|
|
105
|
+
{{else}}
|
|
106
|
+
The door is locked.
|
|
107
|
+
{{/if}}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Globals persist across the whole story. Chapter `locals` are reset and re-computed each time the chapter is entered. Scene `view()` provides render-only values for the current scene.
|
|
111
|
+
|
|
112
|
+
### Images & Resources
|
|
113
|
+
|
|
114
|
+
Reference assets defined in YAML metadata:
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
assets:
|
|
118
|
+
map: "https://example.com/map.png"
|
|
119
|
+
bgm: { url: "https://example.com/audio.mp3", mime: "audio/mpeg" }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```markdown
|
|
123
|
+

|
|
124
|
+
{{asset "bgm"}} → outputs the URL
|
|
125
|
+
{{mime "bgm"}} → outputs "audio/mpeg"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Stylesheets
|
|
129
|
+
|
|
130
|
+
Include CSS via `<style>` tags under the story heading:
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
<style>
|
|
134
|
+
.clue {
|
|
135
|
+
color: #ffd700;
|
|
136
|
+
}
|
|
137
|
+
</style>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Include
|
|
141
|
+
|
|
142
|
+
Use `!include("target")` to splice another Markdown source before parsing:
|
|
143
|
+
|
|
144
|
+
```markdown
|
|
145
|
+
!include("./chapter-1.md")
|
|
146
|
+
!include("/stories/common.md")
|
|
147
|
+
!include("https://example.com/shared.md")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Use `fromPath(pathOrUrl)` to load an entry story and its includes through one path or URL. In Node, relative entry paths are resolved from `cwd`, absolute paths load from the file system, and URLs load over the network. In browsers, relative entry paths resolve from the current page URL, absolute paths resolve from the current origin, and URLs stay unchanged. Includes follow the same rule relative to the file or URL that contains the `!include`. Pass `base` or `resolveInclude` to `fromPath()`, `fromSource()`, or the lower-level `parseStorySource()` when you need custom include loading. You can also use `fromParsed()` to construct a story from an already-parsed structure.
|
|
151
|
+
|
|
152
|
+
### Hooks
|
|
153
|
+
|
|
154
|
+
Hooks are JavaScript functions that run at specific points. Export them from `<script>` tags.
|
|
155
|
+
Each story, chapter, or scene scope may contain at most one `<script>` tag.
|
|
156
|
+
|
|
157
|
+
| Level | Position | Hook | Purpose |
|
|
158
|
+
| ------- | ----------- | -------------------------------------- | --------------------------------------------------------- |
|
|
159
|
+
| Story | Under `#` | `globals()` | Return initial global variables |
|
|
160
|
+
| | | `onStart({ globals })` | Side effect when story begins |
|
|
161
|
+
| Chapter | Under `##` | `locals({ globals })` | Return chapter-local variables |
|
|
162
|
+
| | | `onEnter({ globals, locals })` | Side effect when entering the chapter |
|
|
163
|
+
| | | `onLeave({ globals, locals, target })` | Side effect when leaving the chapter, including story end |
|
|
164
|
+
| Scene | Under `###` | `view({ globals, locals })` | Return render-only values for the scene |
|
|
165
|
+
| | | `onEnter({ globals, locals })` | Side effect on scene enter |
|
|
166
|
+
| | | `onLeave({ globals, locals, target })` | Side effect on scene exit |
|
|
167
|
+
|
|
168
|
+
Hooks with return values support both sync and `async`. `globals()` receives no arguments. `view()` receives the current runtime scopes, but its return value is only used for the current render.
|
|
169
|
+
|
|
170
|
+
### Line Breaks
|
|
171
|
+
|
|
172
|
+
```markdown
|
|
173
|
+
{{linebreak}} ← one blank line
|
|
174
|
+
{{linebreak 3}} ← three blank lines
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Line breaks are renderer-aware: `\n` in Markdown output, `<br>` in HTML output.
|
|
178
|
+
|
|
179
|
+
### CLI
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Play a story interactively in the terminal
|
|
183
|
+
npx mdstory play my-story.md
|
|
184
|
+
|
|
185
|
+
# Play with debug output
|
|
186
|
+
npx mdstory play my-story.md --debug
|
|
187
|
+
|
|
188
|
+
# Build a standalone HTML file and open in browser
|
|
189
|
+
npx mdstory build my-story.md
|
|
190
|
+
|
|
191
|
+
# Build to a specific output path
|
|
192
|
+
npx mdstory build my-story.md -o dist/story.html
|
|
193
|
+
|
|
194
|
+
# Build without opening the browser
|
|
195
|
+
npx mdstory build my-story.md --no-open
|
|
196
|
+
|
|
197
|
+
# Build with debug output in the browser console
|
|
198
|
+
npx mdstory build my-story.md --debug
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Example: Branching Scene
|
|
202
|
+
|
|
203
|
+
```markdown
|
|
204
|
+
### Crossroads {#crossroads}
|
|
205
|
+
|
|
206
|
+
A fork in the road. Which way?
|
|
207
|
+
|
|
208
|
+
{{#nav "chap1.forest"}}🌲 Into the woods{{/nav}}
|
|
209
|
+
{{#nav "chap1.mountain"}}⛰️ Up the mountain{{/nav}}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Examples
|
|
213
|
+
|
|
214
|
+
Full working stories in [examples/](./examples/).
|
|
215
|
+
|
|
216
|
+
### Chapter with Locals and Branching
|
|
217
|
+
|
|
218
|
+
```markdown
|
|
219
|
+
## The Dungeon {#dungeon}
|
|
220
|
+
|
|
221
|
+
<script>
|
|
222
|
+
let attempts = 0;
|
|
223
|
+
export default {
|
|
224
|
+
locals() {
|
|
225
|
+
attempts++;
|
|
226
|
+
return { attempt: attempts };
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
</script>
|
|
230
|
+
|
|
231
|
+
### First Room {#room}
|
|
232
|
+
|
|
233
|
+
You enter the dungeon. This is your {{attempt}}th attempt.
|
|
234
|
+
|
|
235
|
+
{{input "boolean" ready=false}}
|
|
236
|
+
|
|
237
|
+
{{#if ready}}
|
|
238
|
+
The passage splits in two.
|
|
239
|
+
{{#nav "dungeon.left"}}Go left{{/nav}}
|
|
240
|
+
{{#nav "dungeon.right"}}Go right{{/nav}}
|
|
241
|
+
{{else}}
|
|
242
|
+
You're not ready yet.
|
|
243
|
+
{{#nav "dungeon.room"}}Take a breath{{/nav}}
|
|
244
|
+
{{/if}}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Scene with View Hook
|
|
248
|
+
|
|
249
|
+
```markdown
|
|
250
|
+
### Treasure Chest {#chest}
|
|
251
|
+
|
|
252
|
+
<script>
|
|
253
|
+
export default {
|
|
254
|
+
view({ globals }) {
|
|
255
|
+
const opened = globals.chestOpened || false;
|
|
256
|
+
return {
|
|
257
|
+
alreadyOpened: opened,
|
|
258
|
+
coins: opened ? 0 : 50,
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
onLeave({ globals }) {
|
|
262
|
+
globals.chestOpened = true;
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
</script>
|
|
266
|
+
|
|
267
|
+
{{#if alreadyOpened}}
|
|
268
|
+
The chest is empty.
|
|
269
|
+
{{else}}
|
|
270
|
+
You found {{coins}} gold pieces!
|
|
271
|
+
{{/if}}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Cross-Chapter Navigation
|
|
275
|
+
|
|
276
|
+
```markdown
|
|
277
|
+
### Escape {#escape}
|
|
278
|
+
|
|
279
|
+
{{#nav "dungeon.room"}}Go back inside{{/nav}}
|
|
280
|
+
{{#nav "overworld.village"}}Run to the village{{/nav}}
|
|
281
|
+
{{#nav null}}Give up{{/nav}}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Full Story: Simple Choice
|
|
285
|
+
|
|
286
|
+
```markdown
|
|
287
|
+
---
|
|
288
|
+
title: The Crossing
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
# The Crossing
|
|
292
|
+
|
|
293
|
+
### Crossroads {#start}
|
|
294
|
+
|
|
295
|
+
A stranger approaches you.
|
|
296
|
+
|
|
297
|
+
{{input "string" $name="traveler"}}
|
|
298
|
+
|
|
299
|
+
{{#nav "forest.path"}}Enter the forest{{/nav}}
|
|
300
|
+
{{#nav "river.bridge"}}Cross the bridge{{/nav}}
|
|
301
|
+
|
|
302
|
+
## Forest {#forest}
|
|
303
|
+
|
|
304
|
+
### Deep Woods {#path}
|
|
305
|
+
|
|
306
|
+
You walk among ancient trees, {{name}}.
|
|
307
|
+
|
|
308
|
+
The forest whispers your name.
|
|
309
|
+
|
|
310
|
+
{{#nav "start"}}Turn back{{/nav}}
|
|
311
|
+
{{#nav null}}Rest here forever{{/nav}}
|
|
312
|
+
|
|
313
|
+
## River {#river}
|
|
314
|
+
|
|
315
|
+
### Old Bridge {#bridge}
|
|
316
|
+
|
|
317
|
+
The wooden bridge creaks under your weight, {{name}}.
|
|
318
|
+
|
|
319
|
+
On the far side, you see a light.
|
|
320
|
+
|
|
321
|
+
{{#nav "start"}}Go back{{/nav}}
|
|
322
|
+
{{#nav null}}Cross into the light{{/nav}}
|
|
323
|
+
```
|