@elvishscout/mdstory 0.1.1 → 0.1.2

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.
@@ -58,51 +58,57 @@ const createSubmitButtonHtml = ({ tagMap = {}, target, children, }) => {
58
58
  };
59
59
  return createElementHtml(tag, buttonAttrs, children);
60
60
  };
61
- const createInputMarkdown = ({ name, type }) => {
62
- if (type === "boolean") {
63
- return `[ ${name}? ]`;
64
- }
65
- return `[> ${name} <]`;
66
- };
67
- const useHelper = ({ inputs, sets, navs }, options) => {
61
+ const useHelper = ({ inputs, sets, navs }, assets, options) => {
68
62
  return {
69
63
  input(type, opt) {
70
- let result = "";
71
64
  for (const name in opt.hash) {
65
+ let result;
72
66
  const value = opt.hash[name];
73
67
  inputs.push({ name, type, value });
74
68
  if (options.format === "html") {
75
69
  result = createInputHtml({ name, type, value, ...options });
76
70
  }
77
71
  else {
78
- result = createInputMarkdown({ name, type });
72
+ if (type === "boolean") {
73
+ result = `[? _${name}_]`;
74
+ }
75
+ else {
76
+ result = `[> _${name}_]`;
77
+ }
79
78
  }
80
- break;
79
+ return new Handlebars.SafeString(result);
81
80
  }
82
- return new Handlebars.SafeString(result);
81
+ return "";
83
82
  },
84
83
  set(opt) {
85
- const result = Object.entries(opt.hash)
86
- .map(([name, value]) => {
84
+ for (const name in opt.hash) {
85
+ const value = opt.hash[name];
87
86
  const type = valueType(value);
88
87
  sets.push({ name, type, value });
89
- if (options.format === "html") {
90
- return createInputHtml({ name, type, value, readonly: true, ...options });
91
- }
92
- return "";
93
- })
94
- .join("");
95
- return new Handlebars.SafeString(result);
88
+ }
89
+ return "";
96
90
  },
97
91
  nav(target, opt) {
98
- let result = "";
92
+ let result;
99
93
  const text = opt.fn(this).trim();
100
94
  navs.push({ text, target });
101
95
  if (options.format === "html") {
102
96
  result = createSubmitButtonHtml({ target: target ?? "", children: text, ...options });
103
97
  }
98
+ else {
99
+ result = `[@ __${text}__]`;
100
+ }
104
101
  return new Handlebars.SafeString(result);
105
102
  },
103
+ asset(name) {
104
+ return new Handlebars.SafeString(assets[name]?.url ?? "");
105
+ },
106
+ mime(name) {
107
+ return new Handlebars.SafeString(assets[name]?.mime ?? "");
108
+ },
109
+ linebreak(n) {
110
+ return new Handlebars.SafeString("\n".repeat(n ?? 1));
111
+ },
106
112
  };
107
113
  };
108
114
  export class Chapter {
@@ -112,20 +118,23 @@ export class Chapter {
112
118
  this.template = template;
113
119
  this.hooks = hooks;
114
120
  }
115
- render(scope, options) {
121
+ render(scope, assets = {}, options) {
116
122
  const md = new MarkdownIt({ html: true }).use(pluginAttrs);
117
123
  const fields = {
118
124
  inputs: [],
119
125
  sets: [],
120
126
  navs: [],
121
127
  };
122
- const handle = Handlebars.create();
123
- handle.registerHelper(useHelper(fields, options));
124
- let text = handle.compile(this.template)(scope);
128
+ const helpers = useHelper(fields, assets, options);
129
+ let text;
125
130
  if (options.format === "html") {
131
+ text = Handlebars.compile(this.template)(scope, { helpers });
126
132
  text = md.render(text);
127
133
  text = createElementHtml("input", { type: "submit", disabled: true, hidden: true }) + text;
128
134
  }
135
+ else {
136
+ text = Handlebars.compile(this.template, { noEscape: true })(scope, { helpers });
137
+ }
129
138
  return { text, ...fields };
130
139
  }
131
140
  }
@@ -1,9 +1,17 @@
1
1
  import { z } from "zod";
2
2
  export const ValueSchema = z.any().transform((v) => v);
3
3
  export const ScopeSchema = z.record(ValueSchema);
4
+ export const AssetSchema = z.union([
5
+ z.string().transform((url) => ({ url, mime: undefined })),
6
+ z.object({ url: z.string(), mime: z.string().optional() }),
7
+ ]);
8
+ export const AssetsSchema = z.record(AssetSchema);
4
9
  export const MetadataSchema = z.object({
5
- title: z.string().default(""),
6
- globals: ScopeSchema.default({}),
10
+ title: z.string().optional(),
11
+ author: z.string().optional(),
12
+ email: z.string().optional(),
13
+ globals: ScopeSchema.optional(),
14
+ assets: AssetsSchema.optional(),
7
15
  });
8
16
  const TargetSchema = z.string().or(z.null());
9
17
  export const StoryHooksSchema = z
@@ -1,4 +1,5 @@
1
1
  export * from "./definitions.js";
2
2
  export * from "./story.js";
3
+ export * from "./parser.js";
3
4
  export * from "./chapter.js";
4
5
  export * from "./error.js";
@@ -0,0 +1,86 @@
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
+ };
@@ -1,10 +1,6 @@
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, StoryHooksSchema, ChapterHooksSchema, } from "./definitions.js";
1
+ import { ChapterHooksSchema, StoryHooksSchema, } from "./definitions.js";
6
2
  import { Chapter } from "./chapter.js";
7
- import { ChapterNotFoundError, DuplicateIdError, EmptyChapterIdError, InvalidInputError, InvalidMetadataError, } from "./error.js";
3
+ import { ChapterNotFoundError, InvalidInputError } from "./error.js";
8
4
  const parstInput = (type, text) => {
9
5
  if (type === "boolean") {
10
6
  return text === "on";
@@ -30,114 +26,36 @@ const parseFormData = (formData, { inputs, sets }) => {
30
26
  }));
31
27
  return { target, updates };
32
28
  };
33
- export const parseStoryContent = (content) => {
34
- const md = new MarkdownIt({ html: true }).use(pluginAttrs).use(pluginFrontMatter, () => { });
35
- const tokens = md.parse(content, {});
36
- let metadata = MetadataSchema.parse({});
37
- let storyScript = "";
38
- let stylesheet = "";
39
- const ignoredRanges = [];
40
- const divisions = [];
41
- tokens.forEach((token, i) => {
42
- if (token.type === "front_matter" && token.meta) {
43
- try {
44
- const frontMatter = MetadataSchema.parse(yaml.load(token.meta));
45
- metadata = { ...metadata, ...frontMatter };
46
- }
47
- catch {
48
- throw new InvalidMetadataError(token.meta);
49
- }
50
- }
51
- else if (token.type === "heading_open" && token.tag === "h1" && token.level === 0 && token.map) {
52
- const lineno = token.map[0];
53
- let id = token.attrs?.find(([key]) => key === "id")?.[1] ?? "";
54
- let title = "";
55
- const nextToken = tokens[i + 1];
56
- if (nextToken && nextToken.type === "inline") {
57
- const content = nextToken.content.trim();
58
- id || (id = content);
59
- title = content;
60
- }
61
- if (!id) {
62
- throw new EmptyChapterIdError();
63
- }
64
- if (divisions.find(({ id: _id }) => id === _id)) {
65
- throw new DuplicateIdError(id);
66
- }
67
- divisions.push({ id, title, lineno, script: "" });
68
- }
69
- else if (token.type === "html_block" && token.map) {
70
- let regres;
71
- if ((regres = /^[\s]*<script>(.*)<\/script>[\s]*$/s.exec(token.content))) {
72
- const script = regres[1].trim();
73
- if (script) {
74
- if (divisions.length === 0) {
75
- storyScript = script;
76
- }
77
- else {
78
- divisions[divisions.length - 1].script = script;
79
- }
80
- ignoredRanges.push(token.map);
81
- }
82
- }
83
- else if ((regres = /^[\s]*<style>(.*)<\/style>[\s]*$/s.exec(token.content))) {
84
- const style = regres[1].trim();
85
- stylesheet += style;
86
- ignoredRanges.push(token.map);
87
- }
88
- }
89
- });
90
- const lines = content.split("\n").map((line, i) => {
91
- if (ignoredRanges.find(([from, to]) => i >= from && i < to)) {
92
- return null;
93
- }
94
- return line;
95
- });
96
- const chapterEntries = divisions.map(({ id, title, lineno, script }, i) => {
97
- const template = lines
98
- .slice(lineno, divisions[i + 1]?.lineno)
99
- .filter((line) => line !== null)
100
- .join("\n");
101
- const hooks = script ? ChapterHooksSchema.parse(new Function(script)()) : {};
102
- return [id, { title, template, hooks }];
103
- });
104
- const chapters = Object.fromEntries(chapterEntries);
105
- const entry = chapterEntries[0]?.[0] || null;
106
- const hooks = storyScript ? StoryHooksSchema.parse(new Function(storyScript)()) : {};
107
- return {
108
- metadata,
109
- chapters,
110
- entry,
111
- hooks,
112
- stylesheet,
113
- };
114
- };
115
29
  export class StoryBase {
116
- constructor({ metadata, chapters, entry, hooks, stylesheet }) {
117
- const realChapters = Object.fromEntries(Object.entries(chapters).map(([id, options]) => {
118
- return [id, new Chapter({ id, ...options })];
30
+ constructor({ metadata, chapters, entry, script, stylesheet }) {
31
+ const realChapters = Object.fromEntries(Object.entries(chapters).map(([id, { title, template, script }]) => {
32
+ const hooks = (script && ChapterHooksSchema.parse(new Function(script)())) || {};
33
+ return [id, new Chapter({ id, title, template, hooks })];
119
34
  }));
120
35
  this.metadata = metadata;
121
- this.globals = metadata.globals;
36
+ this.globals = metadata.globals ?? {};
122
37
  this.chapters = realChapters;
123
38
  this.entry = (entry && realChapters[entry]) || null;
124
- this.hooks = hooks;
39
+ this.hooks = (script && StoryHooksSchema.parse(new Function(script)())) || {};
125
40
  this.stylesheet = stylesheet;
41
+ this.assets = metadata.assets ?? {};
126
42
  }
127
43
  async play(prompt, options) {
128
44
  if (this.hooks.onStart) {
129
45
  this.hooks.onStart(this.globals);
130
46
  }
47
+ const assetsUrl = Object.fromEntries(Object.entries(this.assets).map(([name, { url }]) => [name, url]));
131
48
  let chapter = this.entry;
132
49
  while (chapter) {
133
- let modifiedGlobals = this.globals;
50
+ let scope = this.globals;
51
+ scope = Object.assign(scope, assetsUrl);
134
52
  if (chapter.hooks.onEnter) {
135
53
  const modified = chapter.hooks.onEnter(this.globals);
136
54
  if (modified !== undefined) {
137
- modifiedGlobals = Object.assign(modifiedGlobals, modified);
55
+ scope = Object.assign(scope, modified);
138
56
  }
139
57
  }
140
- const renderResult = chapter.render(modifiedGlobals, options);
58
+ const renderResult = chapter.render(scope, this.assets, options);
141
59
  const promptResult = await prompt({ chapter, ...renderResult });
142
60
  const { target, updates } = promptResult instanceof FormData ? parseFormData(promptResult, renderResult) : promptResult;
143
61
  let modifiedTarget = target;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elvishscout/mdstory",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "An interactive story format based on Markdown and Handlebars with JavaScript support.",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
@@ -1,4 +1,4 @@
1
- import { ValueType, Value, ChapterHooks, Scope } from "./definitions.js";
1
+ import { ValueType, Value, ChapterHooks, Scope, Asset } from "./definitions.js";
2
2
  type MarkdownOptions = {};
3
3
  type HtmlOptions = {
4
4
  tagMap?: Record<string, string>;
@@ -39,6 +39,6 @@ export declare class Chapter {
39
39
  template: string;
40
40
  hooks: ChapterHooks;
41
41
  constructor({ id, title, template, hooks }: ChapterOptions);
42
- render(scope: Scope, options: RenderOptions): RenderResult;
42
+ render(scope: Scope, assets: Record<string, Asset> | undefined, options: RenderOptions): RenderResult;
43
43
  }
44
44
  export {};
@@ -7,15 +7,71 @@ type JsonObject = {
7
7
  type JsonValue = JsonPrimitive | JsonArray | JsonObject;
8
8
  export declare const ValueSchema: z.ZodEffects<z.ZodAny, string | number | boolean | JsonArray | JsonObject | null, any>;
9
9
  export declare const ScopeSchema: z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodAny, string | number | boolean | JsonArray | JsonObject | null, any>>;
10
+ export declare const AssetSchema: z.ZodUnion<[z.ZodEffects<z.ZodString, {
11
+ url: string;
12
+ mime: undefined;
13
+ }, string>, z.ZodObject<{
14
+ url: z.ZodString;
15
+ mime: z.ZodOptional<z.ZodString>;
16
+ }, "strip", z.ZodTypeAny, {
17
+ url: string;
18
+ mime?: string | undefined;
19
+ }, {
20
+ url: string;
21
+ mime?: string | undefined;
22
+ }>]>;
23
+ export declare const AssetsSchema: z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodEffects<z.ZodString, {
24
+ url: string;
25
+ mime: undefined;
26
+ }, string>, z.ZodObject<{
27
+ url: z.ZodString;
28
+ mime: z.ZodOptional<z.ZodString>;
29
+ }, "strip", z.ZodTypeAny, {
30
+ url: string;
31
+ mime?: string | undefined;
32
+ }, {
33
+ url: string;
34
+ mime?: string | undefined;
35
+ }>]>>;
10
36
  export declare const MetadataSchema: z.ZodObject<{
11
- title: z.ZodDefault<z.ZodString>;
12
- globals: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodAny, string | number | boolean | JsonArray | JsonObject | null, any>>>;
37
+ title: z.ZodOptional<z.ZodString>;
38
+ author: z.ZodOptional<z.ZodString>;
39
+ email: z.ZodOptional<z.ZodString>;
40
+ globals: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodAny, string | number | boolean | JsonArray | JsonObject | null, any>>>;
41
+ assets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodEffects<z.ZodString, {
42
+ url: string;
43
+ mime: undefined;
44
+ }, string>, z.ZodObject<{
45
+ url: z.ZodString;
46
+ mime: z.ZodOptional<z.ZodString>;
47
+ }, "strip", z.ZodTypeAny, {
48
+ url: string;
49
+ mime?: string | undefined;
50
+ }, {
51
+ url: string;
52
+ mime?: string | undefined;
53
+ }>]>>>;
13
54
  }, "strip", z.ZodTypeAny, {
14
- title: string;
15
- globals: Record<string, string | number | boolean | JsonArray | JsonObject | null>;
55
+ title?: string | undefined;
56
+ author?: string | undefined;
57
+ email?: string | undefined;
58
+ globals?: Record<string, string | number | boolean | JsonArray | JsonObject | null> | undefined;
59
+ assets?: Record<string, {
60
+ url: string;
61
+ mime: undefined;
62
+ } | {
63
+ url: string;
64
+ mime?: string | undefined;
65
+ }> | undefined;
16
66
  }, {
17
67
  title?: string | undefined;
68
+ author?: string | undefined;
69
+ email?: string | undefined;
18
70
  globals?: Record<string, any> | undefined;
71
+ assets?: Record<string, string | {
72
+ url: string;
73
+ mime?: string | undefined;
74
+ }> | undefined;
19
75
  }>;
20
76
  export declare const StoryHooksSchema: z.ZodObject<{
21
77
  onStart: z.ZodOptional<z.ZodFunction<z.ZodTuple<[z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodAny, string | number | boolean | JsonArray | JsonObject | null, any>>], z.ZodUnknown>, z.ZodUnion<[z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodAny, string | number | boolean | JsonArray | JsonObject | null, any>>, z.ZodVoid]>>>;
@@ -39,8 +95,22 @@ export declare const ChapterHooksSchema: z.ZodObject<{
39
95
  }>;
40
96
  export type Value = z.infer<typeof ValueSchema>;
41
97
  export type Scope = z.infer<typeof ScopeSchema>;
98
+ export type Asset = z.infer<typeof AssetSchema>;
42
99
  export type Metadata = z.infer<typeof MetadataSchema>;
43
100
  export type StoryHooks = z.infer<typeof StoryHooksSchema>;
44
101
  export type ChapterHooks = z.infer<typeof ChapterHooksSchema>;
45
102
  export type ValueType = "string" | "number" | "boolean" | "object";
103
+ export type ChapterBody = {
104
+ title: string;
105
+ template: string;
106
+ script: string;
107
+ };
108
+ export type StoryBody = {
109
+ metadata: Metadata;
110
+ chapters: Record<string, ChapterBody>;
111
+ entry: string | null;
112
+ script: string;
113
+ stylesheet: string;
114
+ };
115
+ export type StoryAssets = Record<string, string>;
46
116
  export {};
@@ -1,4 +1,5 @@
1
1
  export * from "./definitions.js";
2
2
  export * from "./story.js";
3
+ export * from "./parser.js";
3
4
  export * from "./chapter.js";
4
5
  export * from "./error.js";
@@ -0,0 +1,2 @@
1
+ import { StoryBody } from "./definitions.js";
2
+ export declare const parseStoryContent: (content: string) => StoryBody;
@@ -1,4 +1,4 @@
1
- import { StoryHooks, Scope, Metadata, ChapterHooks } from "./definitions.js";
1
+ import { StoryBody, StoryHooks, Scope, Metadata, Asset } from "./definitions.js";
2
2
  import { Chapter, RenderOptions, RenderResult } from "./chapter.js";
3
3
  export type StoryPrompt = (props: {
4
4
  chapter: Chapter;
@@ -6,19 +6,6 @@ export type StoryPrompt = (props: {
6
6
  target: string | null;
7
7
  updates: Scope;
8
8
  } | FormData>;
9
- type ChapterBody = {
10
- title: string;
11
- template: string;
12
- hooks: ChapterHooks;
13
- };
14
- type StoryBody = {
15
- metadata: Metadata;
16
- chapters: Record<string, ChapterBody>;
17
- entry: string | null;
18
- hooks: StoryHooks;
19
- stylesheet: string;
20
- };
21
- export declare const parseStoryContent: (content: string) => StoryBody;
22
9
  export declare class StoryBase {
23
10
  metadata: Metadata;
24
11
  globals: Scope;
@@ -26,7 +13,7 @@ export declare class StoryBase {
26
13
  entry: Chapter | null;
27
14
  hooks: StoryHooks;
28
15
  stylesheet: string;
29
- constructor({ metadata, chapters, entry, hooks, stylesheet }: StoryBody);
16
+ assets: Record<string, Asset>;
17
+ constructor({ metadata, chapters, entry, script, stylesheet }: StoryBody);
30
18
  play(prompt: StoryPrompt, options: RenderOptions): Promise<void>;
31
19
  }
32
- export {};