@elvishscout/mdstory 0.1.3 → 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.
Files changed (47) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +323 -295
  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 +30 -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/package.json CHANGED
@@ -1,34 +1,54 @@
1
1
  {
2
+ "workspaces": [
3
+ "html-template"
4
+ ],
2
5
  "name": "@elvishscout/mdstory",
3
- "repository": "https://github.com/ElvishScout/mdstory.git",
4
- "version": "0.1.3",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ElvishScout/mdstory.git"
9
+ },
10
+ "version": "0.2.0",
5
11
  "description": "An interactive fiction scripting format based on Markdown and Handlebars.",
6
12
  "main": "./dist/index.js",
13
+ "sideEffects": false,
7
14
  "type": "module",
8
15
  "files": [
9
16
  "./dist",
10
- "./types"
17
+ "./types",
18
+ "./html-template/dist"
11
19
  ],
20
+ "bin": {
21
+ "mdstory": "dist/cli/index.js"
22
+ },
12
23
  "scripts": {
13
- "build": "tsc",
14
- "test": "tsx ./test/index.test.ts"
24
+ "prebuild": "shx rm -rf \"dist/*\" \"dist/.*\"",
25
+ "build": "tsc -b",
26
+ "build:template": "cd html-template && npm run build",
27
+ "build:all": "npm run build && npm run build:template",
28
+ "cli": "node ./dist/cli/index.js"
15
29
  },
16
30
  "devDependencies": {
17
31
  "@types/js-yaml": "^4.0.9",
18
32
  "@types/markdown-it": "^14.1.2",
19
33
  "@types/markdown-it-attrs": "^4.1.3",
20
- "@types/node": "^22.14.1",
21
- "inquirer": "^12.6.0",
22
- "markdown-it-terminal": "^0.4.0",
34
+ "@types/node": "^22.20.0",
35
+ "shx": "^0.4.0",
23
36
  "tsx": "^4.19.3",
24
- "typescript": "~5.7.2"
37
+ "typescript": "~5.7.2",
38
+ "words-count": "^2.0.2"
25
39
  },
26
40
  "dependencies": {
41
+ "commander": "^15.0.0",
27
42
  "handlebars": "^4.7.8",
43
+ "inquirer": "^12.6.0",
28
44
  "js-yaml": "^4.1.0",
29
45
  "markdown-it": "^14.1.0",
30
46
  "markdown-it-attrs": "^4.3.1",
31
47
  "markdown-it-front-matter": "^0.2.4",
48
+ "markdown-it-mark": "^4.0.0",
49
+ "markdown-it-terminal": "^0.4.0",
50
+ "nanoid": "^5.1.16",
51
+ "open": "^10.2.0",
32
52
  "zod": "^3.24.3"
33
53
  },
34
54
  "types": "./types",
@@ -38,4 +58,4 @@
38
58
  ],
39
59
  "author": "Jiakai Jiang",
40
60
  "license": "ISC"
41
- }
61
+ }
@@ -0,0 +1,6 @@
1
+ export interface BuildOptions {
2
+ output?: string;
3
+ open?: boolean;
4
+ debug?: boolean;
5
+ }
6
+ export declare function buildCommand(storyPath: string, options: BuildOptions): Promise<void>;
@@ -0,0 +1,4 @@
1
+ export interface PlayOptions {
2
+ debug?: boolean;
3
+ }
4
+ export declare function playCommand(storyPath: string, options: PlayOptions): Promise<void>;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,2 @@
1
+ import MarkdownIt from "markdown-it";
2
+ export declare function createMarkdownRenderer(): MarkdownIt;
@@ -0,0 +1,3 @@
1
+ import type MarkdownIt from "markdown-it";
2
+ import type { StoryPrompt } from "../index.js";
3
+ export declare function createPrompt(md: MarkdownIt): StoryPrompt;
@@ -0,0 +1,19 @@
1
+ import { Scene } from "./scene.js";
2
+ import type { RenderOptions, RenderResult } from "./render.js";
3
+ import type { ChapterHooks, Scope, Asset, ChapterInit, DEFAULT_CHAPTER } from "./definitions.js";
4
+ import type { ParsedChapter } from "./parser.js";
5
+ /** A chapter grouping scenes with shared hooks and local variables. */
6
+ export declare class Chapter {
7
+ id: string | typeof DEFAULT_CHAPTER;
8
+ title: string;
9
+ template: string;
10
+ hooks: ChapterHooks;
11
+ locals: Scope;
12
+ scenes: Scene[];
13
+ constructor({ id, title, template, hooks, locals, scenes }: ChapterInit);
14
+ static fromParsed(chapter: ParsedChapter): Promise<Chapter>;
15
+ /** Renders the chapter template with the given scope and render options. */
16
+ render(scope: Scope, assets: Record<string, Asset>, options: RenderOptions): RenderResult;
17
+ /** Get scene by scene id */
18
+ getScene(id: string): Scene | null;
19
+ }
@@ -0,0 +1,83 @@
1
+ import type { Chapter } from "./chapter.js";
2
+ import type { Scene } from "./scene.js";
3
+ type JsonPrimitive = number | string | boolean | null;
4
+ type JsonArray = JsonValue[];
5
+ type JsonObject = {
6
+ [key: string]: JsonValue;
7
+ };
8
+ type JsonValue = JsonPrimitive | JsonArray | JsonObject;
9
+ type HookResult<T = void> = T | Promise<T>;
10
+ export type { JsonValue };
11
+ /** All JSON-compatible values. */
12
+ export type Variable = JsonValue;
13
+ /** An object of variable values by their names. */
14
+ export type Scope = JsonObject;
15
+ /** A referenceable resource file. */
16
+ export type Asset = {
17
+ url: string;
18
+ mime?: string;
19
+ alt?: string;
20
+ };
21
+ /** Story metadata. */
22
+ export type Metadata = {
23
+ title?: string;
24
+ author?: string;
25
+ email?: string;
26
+ globals?: Scope;
27
+ assets?: Record<string, Asset>;
28
+ };
29
+ export type HookContext = {
30
+ globals: Scope;
31
+ locals: Scope;
32
+ };
33
+ export type StoryHookContext = Pick<HookContext, "globals">;
34
+ export type LeaveHookContext = HookContext & {
35
+ target: string | null;
36
+ };
37
+ /** Type indicator for input fields. */
38
+ export type InputType = "string" | "number" | "boolean";
39
+ /** Story-level lifecycle hooks. */
40
+ export type StoryHooks = {
41
+ globals?: () => HookResult<Scope | undefined>;
42
+ onStart?: (context: StoryHookContext) => HookResult;
43
+ };
44
+ /** Chapter-level lifecycle hooks. */
45
+ export type ChapterHooks = {
46
+ locals?: (context: StoryHookContext) => HookResult<Scope | undefined>;
47
+ onEnter?: (context: HookContext) => HookResult;
48
+ onLeave?: (context: LeaveHookContext) => HookResult;
49
+ };
50
+ /** Scene-level lifecycle hooks. */
51
+ export type SceneHooks = {
52
+ view?: (context: HookContext) => HookResult<Scope | undefined>;
53
+ onEnter?: (context: HookContext) => HookResult;
54
+ onLeave?: (context: LeaveHookContext) => HookResult;
55
+ };
56
+ /** Symbol key for the implicit default chapter holding orphan scenes. */
57
+ export declare const DEFAULT_CHAPTER: unique symbol;
58
+ /** Structured representation of a scene's content. */
59
+ export type SceneInit = {
60
+ id: string;
61
+ title?: string;
62
+ template: string;
63
+ hooks?: SceneHooks;
64
+ };
65
+ /** Structured representation of a chapter. */
66
+ export type ChapterInit = {
67
+ id: string | typeof DEFAULT_CHAPTER;
68
+ title?: string;
69
+ template?: string;
70
+ hooks?: ChapterHooks;
71
+ locals?: Scope;
72
+ scenes: Scene[];
73
+ };
74
+ /** Structured representation of the full story. */
75
+ export type StoryInit = {
76
+ metadata?: Metadata;
77
+ title?: string;
78
+ template?: string;
79
+ chapters: Chapter[];
80
+ stylesheet?: string;
81
+ hooks?: StoryHooks;
82
+ debug?: boolean;
83
+ };
@@ -1,5 +1,6 @@
1
1
  export * from "./definitions.js";
2
2
  export * from "./story.js";
3
3
  export * from "./parser.js";
4
+ export * from "./scene.js";
4
5
  export * from "./chapter.js";
5
- export * from "./error.js";
6
+ export * from "./render.js";
@@ -0,0 +1,39 @@
1
+ import { DEFAULT_CHAPTER } from "./definitions.js";
2
+ import type { Metadata } from "./definitions.js";
3
+ export type IncludeResolver = (path: string) => string | Promise<string>;
4
+ export type ParseStoryOptions = {
5
+ base: string;
6
+ resolveInclude: IncludeResolver;
7
+ };
8
+ export type ParsedScene = {
9
+ id: string;
10
+ title: string;
11
+ template: string;
12
+ script: string;
13
+ };
14
+ export type ParsedChapter = {
15
+ id: string | typeof DEFAULT_CHAPTER;
16
+ title: string;
17
+ template: string;
18
+ script: string;
19
+ scenes: ParsedScene[];
20
+ };
21
+ export type ParsedStory = {
22
+ metadata: Metadata;
23
+ title: string;
24
+ template: string;
25
+ chapters: ParsedChapter[];
26
+ stylesheet: string;
27
+ script: string;
28
+ debug?: boolean;
29
+ };
30
+ export declare function resolveParseOptions(options?: Partial<ParseStoryOptions>): Promise<ParseStoryOptions>;
31
+ /**
32
+ * Parses a Markdown-formatted story source string into a structured StoryInit.
33
+ *
34
+ * Document structure:
35
+ * - `#` (h1): Optional story title — its `<script>` is story hooks
36
+ * - `##` (h2): Chapters — with chapter hooks, contain scenes
37
+ * - `###` (h3): Scenes — with scene hooks and Handlebars templates
38
+ */
39
+ export declare function parseStorySource(source: string, options?: Partial<ParseStoryOptions>): Promise<ParsedStory>;
@@ -0,0 +1,46 @@
1
+ import type { InputType, Variable, Scope, Asset } from "./definitions.js";
2
+ /** Custom renderer for generating output in different formats. */
3
+ export interface Renderer {
4
+ /** Whether to convert Markdown to HTML */
5
+ html?: boolean;
6
+ /** Render input area */
7
+ input?(options: {
8
+ name: string;
9
+ type: InputType;
10
+ value: string;
11
+ }): string;
12
+ /** Render navigation area */
13
+ nav?(options: {
14
+ target: string | null;
15
+ children: string;
16
+ }): string;
17
+ /** Render line breaks */
18
+ linebreak?(options: {
19
+ n?: number;
20
+ }): string;
21
+ }
22
+ /** Rendering options. */
23
+ export type RenderOptions = {
24
+ renderer: "markdown" | "html" | Renderer;
25
+ };
26
+ /** The rendering result containing rendered text and extracted fields. */
27
+ export type RenderResult = {
28
+ text: string;
29
+ } & Fields;
30
+ type Fields = {
31
+ inputs: {
32
+ name: string;
33
+ type: InputType;
34
+ value: Variable;
35
+ }[];
36
+ navs: {
37
+ text: string;
38
+ target: string | null;
39
+ }[];
40
+ };
41
+ /**
42
+ * Compiles a Handlebars template with built-in helpers, optionally renders
43
+ * through MarkdownIt, and returns the rendered text with extracted fields.
44
+ */
45
+ export declare function renderTemplate(template: string, scope: Scope, assets: Record<string, Asset>, options: RenderOptions): RenderResult;
46
+ export {};
@@ -0,0 +1,20 @@
1
+ import type { SceneHooks, Scope, Asset, SceneInit, DEFAULT_CHAPTER } from "./definitions.js";
2
+ import type { ParsedScene } from "./parser.js";
3
+ import type { RenderOptions, RenderResult } from "./render.js";
4
+ export type { RenderOptions, RenderResult };
5
+ /** Defines a scene with a Handlebars template and lifecycle hooks. */
6
+ export declare class Scene {
7
+ id: string;
8
+ title: string;
9
+ template: string;
10
+ hooks: SceneHooks;
11
+ constructor({ id, title, template, hooks }: SceneInit);
12
+ static fromParsed(scene: ParsedScene, chapterId: string | typeof DEFAULT_CHAPTER): Promise<Scene>;
13
+ /**
14
+ * Renders the scene content using the given scope and render options.
15
+ * @param scope - Variables available to the Handlebars template.
16
+ * @param assets - Asset objects keyed by name.
17
+ * @param options - Rendering options (format, html).
18
+ */
19
+ render(scope: Scope, assets: Record<string, Asset>, options: RenderOptions): RenderResult;
20
+ }
@@ -0,0 +1,92 @@
1
+ import { z } from "zod";
2
+ import type { JsonValue, StoryHooks, ChapterHooks, SceneHooks } from "./definitions.js";
3
+ export declare const VariableSchema: z.ZodEffects<z.ZodAny, string | number | boolean | JsonValue[] | {
4
+ [key: string]: JsonValue;
5
+ } | null, any>;
6
+ export declare const ScopeSchema: z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodAny, string | number | boolean | JsonValue[] | {
7
+ [key: string]: JsonValue;
8
+ } | null, any>>;
9
+ export declare const AssetSchema: z.ZodUnion<[z.ZodEffects<z.ZodString, {
10
+ url: string;
11
+ mime?: string | undefined;
12
+ alt?: string | undefined;
13
+ }, string>, z.ZodObject<{
14
+ url: z.ZodString;
15
+ mime: z.ZodOptional<z.ZodString>;
16
+ alt: z.ZodOptional<z.ZodString>;
17
+ }, "strip", z.ZodTypeAny, {
18
+ url: string;
19
+ mime?: string | undefined;
20
+ alt?: string | undefined;
21
+ }, {
22
+ url: string;
23
+ mime?: string | undefined;
24
+ alt?: string | undefined;
25
+ }>]>;
26
+ export declare const AssetsSchema: z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodEffects<z.ZodString, {
27
+ url: string;
28
+ mime?: string | undefined;
29
+ alt?: string | undefined;
30
+ }, string>, z.ZodObject<{
31
+ url: z.ZodString;
32
+ mime: z.ZodOptional<z.ZodString>;
33
+ alt: z.ZodOptional<z.ZodString>;
34
+ }, "strip", z.ZodTypeAny, {
35
+ url: string;
36
+ mime?: string | undefined;
37
+ alt?: string | undefined;
38
+ }, {
39
+ url: string;
40
+ mime?: string | undefined;
41
+ alt?: string | undefined;
42
+ }>]>>;
43
+ export declare const MetadataSchema: z.ZodObject<{
44
+ title: z.ZodOptional<z.ZodString>;
45
+ author: z.ZodOptional<z.ZodString>;
46
+ email: z.ZodOptional<z.ZodString>;
47
+ globals: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodAny, string | number | boolean | JsonValue[] | {
48
+ [key: string]: JsonValue;
49
+ } | null, any>>>;
50
+ assets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodEffects<z.ZodString, {
51
+ url: string;
52
+ mime?: string | undefined;
53
+ alt?: string | undefined;
54
+ }, string>, z.ZodObject<{
55
+ url: z.ZodString;
56
+ mime: z.ZodOptional<z.ZodString>;
57
+ alt: z.ZodOptional<z.ZodString>;
58
+ }, "strip", z.ZodTypeAny, {
59
+ url: string;
60
+ mime?: string | undefined;
61
+ alt?: string | undefined;
62
+ }, {
63
+ url: string;
64
+ mime?: string | undefined;
65
+ alt?: string | undefined;
66
+ }>]>>>;
67
+ }, "strip", z.ZodTypeAny, {
68
+ title?: string | undefined;
69
+ author?: string | undefined;
70
+ email?: string | undefined;
71
+ globals?: Record<string, string | number | boolean | JsonValue[] | {
72
+ [key: string]: JsonValue;
73
+ } | null> | undefined;
74
+ assets?: Record<string, {
75
+ url: string;
76
+ mime?: string | undefined;
77
+ alt?: string | undefined;
78
+ }> | undefined;
79
+ }, {
80
+ title?: string | undefined;
81
+ author?: string | undefined;
82
+ email?: string | undefined;
83
+ globals?: Record<string, any> | undefined;
84
+ assets?: Record<string, string | {
85
+ url: string;
86
+ mime?: string | undefined;
87
+ alt?: string | undefined;
88
+ }> | undefined;
89
+ }>;
90
+ export declare const StoryHooksSchema: z.ZodType<StoryHooks>;
91
+ export declare const ChapterHooksSchema: z.ZodType<ChapterHooks>;
92
+ export declare const SceneHooksSchema: z.ZodType<SceneHooks>;
@@ -0,0 +1,54 @@
1
+ import type { StoryInit, StoryHooks, Scope, Metadata, Asset } from "./definitions.js";
2
+ import { Scene } from "./scene.js";
3
+ import { Chapter } from "./chapter.js";
4
+ import type { RenderOptions, RenderResult } from "./render.js";
5
+ import type { ParsedStory, ParseStoryOptions } from "./parser.js";
6
+ /**
7
+ * Prompt function for handling user input during story playback.
8
+ * Receives the current scene and render result, returns navigation target and submitted input values.
9
+ */
10
+ export type StoryPrompt = (props: {
11
+ scene: Scene;
12
+ } & RenderResult) => Promise<{
13
+ target: string | null;
14
+ inputs: Scope;
15
+ } | FormData>;
16
+ export type PlayOptions = RenderOptions & {
17
+ debug?: boolean;
18
+ };
19
+ /**
20
+ * Story runtime containing core playback logic.
21
+ * Construct via `fromSource(source)`, `fromPath(path)`,
22
+ * `fromParsed(parsedStory)`, or manually with a parsed StoryInit.
23
+ */
24
+ export declare class Story {
25
+ metadata: Metadata;
26
+ title: string;
27
+ template: string;
28
+ globals: Scope;
29
+ assets: Record<string, Asset>;
30
+ hooks: StoryHooks;
31
+ stylesheet: string;
32
+ chapters: Chapter[];
33
+ debug: boolean;
34
+ /** @internal */ _storyTitleShown: boolean;
35
+ constructor(init: StoryInit);
36
+ /** Get chapter by chapter id */
37
+ getChapter(id: string): Chapter | null;
38
+ resolveTarget(target: string, currentChapter: Chapter): {
39
+ chapter: Chapter;
40
+ scene: Scene;
41
+ } | null;
42
+ /** Renders the story template with the given scope and render options. */
43
+ render(scope: Scope, assets: Record<string, Asset>, options: RenderOptions): RenderResult;
44
+ private enterChapter;
45
+ private leaveChapter;
46
+ /** Starts playing the story, looping through chapters and scenes until navigation ends. */
47
+ play(prompt: StoryPrompt, options: PlayOptions): Promise<void>;
48
+ }
49
+ /** Creates a Story instance from a parsed story object. */
50
+ export declare function fromParsed(story: ParsedStory): Promise<Story>;
51
+ /** Parses a story source string and creates a Story instance. */
52
+ export declare function fromSource(source: string, options?: Partial<ParseStoryOptions>): Promise<Story>;
53
+ /** Loads a story from a path or URL and resolves includes relative to each containing resource. */
54
+ export declare function fromPath(path: string, options?: Partial<ParseStoryOptions>): Promise<Story>;
@@ -0,0 +1,7 @@
1
+ import { DEFAULT_CHAPTER } from "./definitions.js";
2
+ export declare function getScriptModuleId(chapterId?: string | typeof DEFAULT_CHAPTER, sceneId?: string): string;
3
+ export declare function importScriptModule(script: string, id?: string): Promise<any>;
4
+ export declare function parseScript<T>(script: string, schema: Zod.ZodType<T>, id: string): Promise<T>;
5
+ export declare function normalizePath(path: string, base?: string): Promise<any>;
6
+ export declare function loadSource(normalizedPath: string): Promise<string>;
7
+ export declare function escapeHtml(text: string): string;
package/types/index.d.ts CHANGED
@@ -1,5 +1 @@
1
- export * from "./base/index.js";
2
- import { StoryBase } from "./base/index.js";
3
- export declare class Story extends StoryBase {
4
- constructor(content: string);
5
- }
1
+ export * from "./core/index.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -1,29 +0,0 @@
1
- import { z } from "zod";
2
- export const ValueSchema = z.any().transform((v) => v);
3
- export const ScopeSchema = z.record(ValueSchema);
4
- const AssetObjectSchema = z.object({ url: z.string(), mime: z.string().optional() });
5
- export const AssetSchema = z.union([
6
- z.string().transform((url) => AssetObjectSchema.parse({ url, mime: undefined })),
7
- AssetObjectSchema,
8
- ]);
9
- export const AssetsSchema = z.record(AssetSchema);
10
- export const MetadataSchema = z.object({
11
- title: z.string().optional(),
12
- author: z.string().optional(),
13
- email: z.string().optional(),
14
- globals: ScopeSchema.optional(),
15
- assets: AssetsSchema.optional(),
16
- });
17
- const TargetSchema = z.string().or(z.null());
18
- export const StoryHooksSchema = z
19
- .object({
20
- onStart: z.function().args(ScopeSchema).returns(ScopeSchema.or(z.void())),
21
- })
22
- .partial();
23
- export const ChapterHooksSchema = z
24
- .object({
25
- onEnter: z.function().args(ScopeSchema).returns(ScopeSchema.or(z.void())),
26
- onLeave: z.function().args(ScopeSchema, ScopeSchema).returns(ScopeSchema.or(z.void())),
27
- onNavigate: z.function().args(TargetSchema, ScopeSchema, ScopeSchema).returns(TargetSchema.or(z.void())),
28
- })
29
- .partial();
@@ -1,30 +0,0 @@
1
- export class InvalidMetadataError extends Error {
2
- constructor(content, message) {
3
- super(message ?? `Invalid metadata:\n\`\`\`\n${content}\n\`\`\``);
4
- this.content = content;
5
- }
6
- }
7
- export class DuplicateIdError extends Error {
8
- constructor(id, message) {
9
- super(message ?? `Two chapters with the same id \`${id}\`.`);
10
- this.id = id;
11
- }
12
- }
13
- export class EmptyChapterIdError extends Error {
14
- constructor(message) {
15
- super(message ?? `Chapter id cannot be empty, either use an non-empty chapter title or explicitly specify an id.`);
16
- }
17
- }
18
- export class ChapterNotFoundError extends Error {
19
- constructor(target, message) {
20
- super(message ?? `Chapter \`${target}\` not found.`);
21
- this.target = target;
22
- }
23
- }
24
- export class InvalidInputError extends Error {
25
- constructor(name, input, message) {
26
- super(message ?? `Invalid input \`${input}\` for variable \`${name}\`.`);
27
- this.name = name;
28
- this.input = input;
29
- }
30
- }
@@ -1,86 +0,0 @@
1
- import yaml from "js-yaml";
2
- import MarkdownIt from "markdown-it";
3
- import pluginFrontMatter from "markdown-it-front-matter";
4
- import pluginAttrs from "markdown-it-attrs";
5
- import { MetadataSchema } from "./definitions.js";
6
- import { DuplicateIdError, EmptyChapterIdError, InvalidMetadataError } from "./error.js";
7
- export const parseStoryContent = (content) => {
8
- const md = new MarkdownIt({ html: true }).use(pluginAttrs).use(pluginFrontMatter, () => { });
9
- const tokens = md.parse(content, {});
10
- let metadata = MetadataSchema.parse({});
11
- let storyScript = "";
12
- let stylesheet = "";
13
- const ignoredRanges = [];
14
- const divisions = [];
15
- tokens.forEach((token, i) => {
16
- if (token.type === "front_matter" && token.meta) {
17
- try {
18
- const frontMatter = MetadataSchema.parse(yaml.load(token.meta));
19
- metadata = Object.assign(metadata, frontMatter);
20
- }
21
- catch {
22
- throw new InvalidMetadataError(token.meta);
23
- }
24
- }
25
- else if (token.type === "heading_open" && token.tag === "h1" && token.level === 0 && token.map) {
26
- const lineno = token.map[0];
27
- let id = token.attrs?.find(([key]) => key === "id")?.[1] ?? "";
28
- let title = "";
29
- const nextToken = tokens[i + 1];
30
- if (nextToken && nextToken.type === "inline") {
31
- const content = nextToken.content.trim();
32
- id || (id = content);
33
- title = content;
34
- }
35
- if (!id) {
36
- throw new EmptyChapterIdError();
37
- }
38
- if (divisions.find(({ id: _id }) => id === _id)) {
39
- throw new DuplicateIdError(id);
40
- }
41
- divisions.push({ id, title, lineno, script: "" });
42
- }
43
- else if (token.type === "html_block" && token.map) {
44
- let regres;
45
- if ((regres = /^[\s]*<script>(.*)<\/script>[\s]*$/s.exec(token.content))) {
46
- const script = regres[1].trim();
47
- if (script) {
48
- if (divisions.length === 0) {
49
- storyScript = script;
50
- }
51
- else {
52
- divisions[divisions.length - 1].script = script;
53
- }
54
- ignoredRanges.push(token.map);
55
- }
56
- }
57
- else if ((regres = /^[\s]*<style>(.*)<\/style>[\s]*$/s.exec(token.content))) {
58
- const style = regres[1].trim();
59
- stylesheet += style;
60
- ignoredRanges.push(token.map);
61
- }
62
- }
63
- });
64
- const lines = content.split("\n").map((line, i) => {
65
- if (ignoredRanges.find(([from, to]) => i >= from && i < to)) {
66
- return null;
67
- }
68
- return line;
69
- });
70
- const chapterEntries = divisions.map(({ id, title, lineno, script }, i) => {
71
- const template = lines
72
- .slice(lineno, divisions[i + 1]?.lineno)
73
- .filter((line) => line !== null)
74
- .join("\n");
75
- return [id, { title, template, script }];
76
- });
77
- const chapters = Object.fromEntries(chapterEntries);
78
- const entry = chapterEntries[0]?.[0] || null;
79
- return {
80
- metadata,
81
- chapters,
82
- entry,
83
- script: storyScript,
84
- stylesheet,
85
- };
86
- };