@elvishscout/mdstory 0.1.4 → 0.2.1

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 (47) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +323 -438
  3. package/README.zh-CN.md +323 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/cli/commands/build.js +33 -0
  6. package/dist/cli/commands/play.js +9 -0
  7. package/dist/cli/index.js +44 -0
  8. package/dist/cli/markdown.js +27 -0
  9. package/dist/cli/prompt.js +44 -0
  10. package/dist/core/chapter.js +31 -0
  11. package/dist/core/definitions.js +2 -0
  12. package/dist/{base → core}/index.js +2 -1
  13. package/dist/core/parser.js +228 -0
  14. package/dist/{base/chapter.js → core/render.js} +45 -63
  15. package/dist/core/scene.js +28 -0
  16. package/dist/core/schema.js +43 -0
  17. package/dist/core/story.js +247 -0
  18. package/dist/core/utils.js +66 -0
  19. package/dist/index.js +1 -7
  20. package/dist/tools/count-words.js +22 -0
  21. package/html-template/dist/index.html +73 -0
  22. package/package.json +31 -10
  23. package/types/cli/commands/build.d.ts +6 -0
  24. package/types/cli/commands/play.d.ts +4 -0
  25. package/types/cli/index.d.ts +2 -0
  26. package/types/cli/markdown.d.ts +2 -0
  27. package/types/cli/prompt.d.ts +3 -0
  28. package/types/core/chapter.d.ts +19 -0
  29. package/types/core/definitions.d.ts +83 -0
  30. package/types/{base → core}/index.d.ts +2 -1
  31. package/types/core/parser.d.ts +39 -0
  32. package/types/core/render.d.ts +46 -0
  33. package/types/core/scene.d.ts +20 -0
  34. package/types/core/schema.d.ts +92 -0
  35. package/types/core/story.d.ts +54 -0
  36. package/types/core/utils.d.ts +7 -0
  37. package/types/index.d.ts +1 -5
  38. package/types/tools/count-words.d.ts +1 -0
  39. package/dist/base/definitions.js +0 -29
  40. package/dist/base/error.js +0 -30
  41. package/dist/base/parser.js +0 -86
  42. package/dist/base/story.js +0 -82
  43. package/types/base/chapter.d.ts +0 -50
  44. package/types/base/definitions.d.ts +0 -113
  45. package/types/base/error.d.ts +0 -20
  46. package/types/base/parser.d.ts +0 -2
  47. package/types/base/story.d.ts +0 -19
package/README.md CHANGED
@@ -1,438 +1,323 @@
1
- # MdStory
2
-
3
- An interactive fiction scripting format based on Markdown and Handlebars.
4
-
5
- Online demo: <https://mdstory.elvish.cc>
6
-
7
- ## Features
8
-
9
- - Seamless integration of Markdown, Handlebars and JavaScript.
10
- - Ease-to-use API, for both Web and command line applications.
11
-
12
- ## File Format
13
-
14
- ### Metadata
15
-
16
- The story file header may include YAML-formatted [Metadata](#type-metadata). Metadata defines the basic information and global configuration of the story.
17
-
18
- ### Chapters
19
-
20
- Story files are divided into chapters by level-one headings. The `id` attribute of the heading serves as the unique identifier (`id`) for the chapter. If omitted, the heading content is used as the chapter `id` instead.
21
-
22
- Handlebars template language is supported in the chapter content. During rendering, properties in `globals` and `assets` are added to the context for direct access by the template.
23
-
24
- ### Helpers
25
-
26
- MdStory includes built-in Handlebars helpers for creating interactive components or various functionalities:
27
-
28
- #### `{{input type name=default}}`
29
-
30
- Creates an input of type `type` and assigns the input value (by default `default`) to a global variable named `name`. Supported input types include: `string`, `number`, `boolean`, and `object` (represented as a JSON string).
31
-
32
- In HTML rendering mode, it generates a corresponding `<input>` element.
33
-
34
- #### `{{set name=value}}`
35
-
36
- Assigns the value `value` to a global variable named `name`.
37
-
38
- #### `{{#nav target}}`
39
-
40
- Creates an entry to navigate to another chapter identified by `target`.
41
-
42
- In HTML rendering mode, it generates a corresponding `<button type="submit">` element.
43
-
44
- #### `{{linebreak [n]}}`
45
-
46
- Inserts `n` blank lines, where `n` defaults to `1`.
47
-
48
- #### `{{asset name}}`
49
-
50
- Retrieves the URL of a resource file named `name`. Basically it works the same as `{{name}}` except that it supports resource names with special characters.
51
-
52
- #### `{{mime name}}`
53
-
54
- Retrieves the MIME type of a resource file named `name`.
55
-
56
- ### JavaScript Integration
57
-
58
- JavaScript may be included into the story source using the `<script>` tag. Scripts before any level-one headings are considered global scripts, while those within chapters are considered chapter scripts. Only one `<script>` tag is allowed at the beginning of the story and in each chapter.
59
-
60
- Scripts are evaluated during runtime using the `Function()` constructor, therefore you may use top-level `return` statements to return objects of type [StoryHooks](#type-storyhooks) or [ChapterHooks](#type-chapterhooks).
61
-
62
- ### Stylesheets
63
-
64
- CSS stylesheets may be included using the `<style>` tag, which are extracted and gathered into the `stylesheet` property of [StoryBody](#type-storybody) during parsing.
65
-
66
- ## Examples
67
-
68
- Here are some [examples](./examples/) of story files.
69
-
70
- ## API
71
-
72
- ### `type Value`
73
-
74
- ```typescript
75
- type JsonPrimitive = number | string | boolean | null;
76
- type JsonArray = JsonValue[];
77
- type JsonObject = { [key: string]: JsonValue };
78
- type JsonValue = JsonPrimitive | JsonArray | JsonObject;
79
- type Value = JsonValue;
80
- ```
81
-
82
- The type of variables in MdStory, basically all types allowed in JSON, including primitive values (`string`, `number`, `boolean`, `null`) and nested `array`s and `object`s. Functions and `undefined` are not supported.
83
-
84
- ### `type ValueType`
85
-
86
- ```typescript
87
- type ValueType = "string" | "number" | "boolean" | "object";
88
- ```
89
-
90
- The type indicator for input fields.
91
-
92
- ### `type Scope`
93
-
94
- ```typescript
95
- type Scope = JsonObject;
96
- ```
97
-
98
- An object of variable values by their names, used to store global variables or input fields.
99
-
100
- ### `type Asset`
101
-
102
- ```typescript
103
- type Asset = { url: string; mime?: string };
104
- ```
105
-
106
- A referenceable resource file.
107
-
108
- #### Properties
109
-
110
- - `url`: The URL of the resource.
111
- - `mime (optional)`: The MIME type of the resource.
112
-
113
- ### `type Metadata`
114
-
115
- ```typescript
116
- type Metadata = {
117
- title?: string;
118
- author?: string;
119
- email?: string;
120
- assets?: Record<string, Asset>;
121
- };
122
- ```
123
-
124
- The Metadata of the story.
125
-
126
- #### Properties
127
-
128
- - `title (optional)`: The story title.
129
- - `author (optional)`: The author's name.
130
- - `email (optional)`: The author's email address.
131
- - `globals (optional)`: A [Scope](#type-scope) containing initial values of global variables.
132
- - `assets (optional)`: An object of all [Asset](#type-asset) objects used in the story by their names.
133
-
134
- ### `type ChapterBody`
135
-
136
- ```typescript
137
- type ChapterBody = {
138
- title: string;
139
- template: string;
140
- script: string;
141
- };
142
- ```
143
-
144
- The structured representation of chapter content.
145
-
146
- #### Properties
147
-
148
- - `title`: The chapter title.
149
- - `template`: The Handlebars template for rendering.
150
- - `script`: The JavaScript script for the chapter.
151
-
152
- ### `type StoryBody`
153
-
154
- ```typescript
155
- type StoryBody = {
156
- metadata: Metadata;
157
- chapters: Record<string, ChapterBody>;
158
- entry: string | null;
159
- script: string;
160
- stylesheet: string;
161
- };
162
- ```
163
-
164
- The structured representation of story content.
165
-
166
- #### Properties
167
-
168
- - `metadata`: The story [Metadata](#type-metadata).
169
- - `chapters`: An object of [ChapterBody](#type-chapterbody) objects by their `id`s.
170
- - `entry`: The `id` of the entry chapter of the story, which may be `null`.
171
- - `script`: The global JavaScript script of the story.
172
- - `stylesheet`: The global stylesheet of the story.
173
-
174
- ### `type StoryHooks`
175
-
176
- ```typescript
177
- type StoryHooks = {
178
- onStart?: (globals: Scope) => Scope | void;
179
- };
180
- ```
181
-
182
- Defines the global lifecycle hooks of the story.
183
-
184
- #### Methods
185
-
186
- - `onStart (optional)`: Called when the story starts.
187
- - Parameters:
188
- - `globals`: A [Scope](#type-scope) of initial global variables.
189
- - Return value:
190
- - Updated [Scope](#type-scope) of global variables or `void`.
191
-
192
- ### `type ChapterHooks`
193
-
194
- ```typescript
195
- type ChapterHooks = {
196
- onEnter?: (globals: Scope) => Scope | void;
197
- onLeave?: (globals: Scope, fields: Scope) => Scope | void;
198
- onNavigate?: (target: string | null, globals: Scope, fields: Scope) => string | null;
199
- };
200
- ```
201
-
202
- Defines the lifecycle hooks of a chapter.
203
-
204
- #### Methods
205
-
206
- - `onEnter (optional)`: Called when entering a chapter.
207
- - Parameters:
208
- - `globals`: A [Scope](#type-scope) of current global variables.
209
- - Return value:
210
- - Updated [Scope](#type-scope) of global variables or `void`. Such update only applies to the current lifecycle of the chapter.
211
- - `onLeave (optional)`: Called when leaving a chapter.
212
- - Parameters:
213
- - `globals`: A [Scope](#type-scope) of current global variables.
214
- - `fields`: A [Scope](#type-scope) of input fields from current chapter.
215
- - Return value:
216
- - Updated [Scope](#type-scope) of global variables or `void`.
217
- - `onNavigate (optional)`: Called during chapter navigation.
218
- - Parameters:
219
- - `target` : `id` of the target chapter.
220
- - `globals`: A [Scope](#type-scope) of current global variables.
221
- - `fields`: A [Scope](#type-scope) of input fields from current chapter.
222
- - Return value:
223
- - Updated `target` or `void`.
224
-
225
- ### `type Renderer`
226
-
227
- ```typescript
228
- type Renderer = {
229
- input?: (options: { name: string; type: ValueType; value: Value }) => string;
230
- nav?: (options: { target: string; children: string }) => string;
231
- };
232
- ```
233
-
234
- Defines the renderer interface for generating outputs in different formats.
235
-
236
- #### Methods
237
-
238
- - `input (optional)`: Generates the rendering result of an input field.
239
- - Parameters:
240
- - `name`: The name of the variable.
241
- - `type`: The [ValueType](#type-valuetype) of the variable.
242
- - `value`: The default [Value](#type-value) of the variable.
243
- - Return value:
244
- - The rendered input field.
245
-
246
- - `nav (optional)`: Generates the rendering result of chapter navigation.
247
- - Parameters:
248
- - `target`: The `id` of the target chapter.
249
- - `children`: The text content of the navigation button.
250
- - Return value:
251
- - The rendered navigation button.
252
-
253
- ### `type RenderOptions`
254
-
255
- ```typescript
256
- type RenderOptions = {
257
- format: "markdown" | "html" | Renderer;
258
- html?: boolean;
259
- };
260
- ```
261
-
262
- Defines rendering options.
263
-
264
- #### Properties
265
-
266
- - `format`: The rendering format, which may be `"markdown"`, `"html"`, or a custom [Renderer](#type-renderer).
267
- - `html (optional)`: Whether to parse Markdown into HTML, defaults to `false`.
268
-
269
- ### `type RenderResult`
270
-
271
- ```typescript
272
- type RenderResult = {
273
- text: string;
274
- inputs: { name: string; type: ValueType; value: Value }[];
275
- navs: { text: string; target: string | null }[];
276
- };
277
- ```
278
-
279
- The rendering result, containing the rendered text and extracted fields.
280
-
281
- #### Properties
282
-
283
- - `text`: The rendered text content.
284
- - `inputs`: An array of input fields, each containing:
285
- - `name`: The name of the variable.
286
- - `type`: The [ValueType](#type-valuetype) of the variable.
287
- - `value`: The default [Value](#type-value) of the variable.
288
- - `navs`: An array of navigation fields, each containing:
289
- - `text`: The text content of the navigation button.
290
- - `target`: The `id` of the target chapter.
291
-
292
- ### `type ChapterOptions`
293
-
294
- ```typescript
295
- type ChapterOptions = {
296
- id: string;
297
- title: string;
298
- template: string;
299
- hooks: ChapterHooks;
300
- };
301
- ```
302
-
303
- Defines the initialization options of a chapter.
304
-
305
- #### Properties
306
-
307
- - `id`: The unique identifier of the chapter.
308
- - `title`: The title of the chapter.
309
- - `template`: The Handlebars template for rendering.
310
- - `hooks`: An object of type [ChapterHooks](#type-chapterhooks).
311
-
312
- ### `class Chapter`
313
-
314
- ```typescript
315
- class Chapter {
316
- id: string;
317
- title: string;
318
- template: string;
319
- hooks: ChapterHooks;
320
-
321
- constructor(options: ChapterOptions);
322
- render(scope: Scope, assets: Record<string, Asset>, options: RenderOptions): string;
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
+ ![]({asset "map"})
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
+ ```