storybook-addon-md 0.3.0 → 0.4.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/CHANGELOG.md +8 -0
- package/README.md +66 -3
- package/STYLING.md +2 -2
- package/dist/content-CKQsa_HK.d.ts +44 -0
- package/dist/content-CTcZAfoF.js +174 -0
- package/dist/index-BzM0tjFM.d.ts +12 -0
- package/dist/index.d.ts +2 -10
- package/dist/node.d.ts +2 -0
- package/dist/node.js +2 -0
- package/dist/preset.d.ts +7 -28
- package/dist/preset.js +13 -156
- package/dist/runtime.d.ts +6 -2
- package/dist/runtime.js +15 -5
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# storybook-addon-md
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- df081eb: Respect `docs.defaultName`, use leading H1s as standalone titles, and add `tagFields` and the `storybook-addon-md/node` parsing API. Markdown pages retain props, examples, and original manifest source.
|
|
8
|
+
|
|
9
|
+
Migration: update attached links from `--markdown` to `--docs` (or the configured name), enable Autodocs in preview-level tags, and render the supplied `heading` in custom layouts. MCP component IDs are unchanged.
|
|
10
|
+
|
|
3
11
|
## 0.3.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -61,7 +61,19 @@ Use buttons to trigger actions.
|
|
|
61
61
|
- Confirm a choice.
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
The component gets
|
|
64
|
+
The component gets one **Docs** entry with Markdown, status and tag chips, examples, and automatic props. The name follows Storybook's `docs.defaultName`, falling back to `Docs`.
|
|
65
|
+
|
|
66
|
+
Enable Autodocs at project level in `.storybook/preview.ts`:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
export default {
|
|
70
|
+
tags: ['autodocs'],
|
|
71
|
+
};
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Storybook 10.6.0 replaces project-level Autodocs with the attached Markdown page. Components without Markdown keep ordinary Autodocs. Use `tags: ['!autodocs']` on components that should not have automatic docs. Component-only or story-only `autodocs` tags conflict with attached MDX in this Storybook version; move the enabling tag to the preview. Native props and examples on Markdown pages do not require an `autodocs` tag.
|
|
75
|
+
|
|
76
|
+
Authored MDX keeps its title and explicit `name`. Additional attached MDX pages can use a distinct name such as `<Meta of={ButtonStories} name="Design notes" />`. An authored page using the default docs name for the same component conflicts with the addon page: keep the authored page and exclude that component's Markdown from `patterns` (or remove its association). The addon does not overwrite authored pages.
|
|
65
77
|
|
|
66
78
|
The sibling convention supports `.stories.tsx`, `.stories.ts`, `.stories.jsx`, and `.stories.js`. To associate a different file, or share a document across components, set `stories` relative to the Markdown file:
|
|
67
79
|
|
|
@@ -89,6 +101,8 @@ Write ordinary Markdown here.
|
|
|
89
101
|
|
|
90
102
|
Without a title, `docs/Introduction.md` appears at `Documentation/docs/Introduction`.
|
|
91
103
|
|
|
104
|
+
A leading Markdown H1 supplies the visible title, including inline formatting. Otherwise the final segment of `title` is shown. Both `# Heading` and Setext H1 syntax work; a later H1 is ordinary content. Sidebar placement and IDs always use the configured or inferred sidebar title. The original file and manifest content remain unchanged.
|
|
105
|
+
|
|
92
106
|
### Frontmatter
|
|
93
107
|
|
|
94
108
|
YAML frontmatter is optional. Use lowercase field names.
|
|
@@ -113,12 +127,59 @@ Invalid frontmatter, missing or ambiguous story references, and missing local as
|
|
|
113
127
|
| `generatedDir` | `storybook-markdown-generated` | Disposable output folder under the working directory. |
|
|
114
128
|
| `stylesheet` | None | Custom stylesheet path. |
|
|
115
129
|
| `manifests` | `false` | Include original Markdown in Storybook documentation manifests. |
|
|
130
|
+
| `tagFields` | `[]` | Additional frontmatter fields displayed as tags. |
|
|
116
131
|
| `presentation` | None | Module exporting `Layout` and/or `MarkdownRenderer`. |
|
|
117
132
|
|
|
118
133
|
Globs and customization paths start from your project folder (the parent of `.storybook` by default). Keep the config and local files inside that folder. Restart Storybook after changing options.
|
|
119
134
|
|
|
120
135
|
`generatedDir` must be a visible folder name using letters, digits, hyphens, or underscores. Hidden folders, nested paths, `node_modules`, and `storybook-static` are unsupported. Ignore the folder in Git; the addon manages its contents.
|
|
121
136
|
|
|
137
|
+
### Docs names and metadata tags
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const config: StorybookConfig = {
|
|
141
|
+
framework: '@storybook/react-vite',
|
|
142
|
+
docs: { defaultName: 'Reference' },
|
|
143
|
+
stories: ['../src/**/*.stories.@(ts|tsx|js|jsx)', '../docs/**/*.mdx'],
|
|
144
|
+
addons: [
|
|
145
|
+
'@storybook/addon-docs',
|
|
146
|
+
{
|
|
147
|
+
name: 'storybook-addon-md',
|
|
148
|
+
options: {
|
|
149
|
+
patterns: ['src/**/*.md', 'docs/**/*.md'],
|
|
150
|
+
tagFields: ['category', 'subcategory'],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`tagFields` adds string values and string array items from those fields to the existing tags. Missing fields, blank strings, and non-string values are ignored. Labels are deduplicated by exact value across documents, tags, configured fields, and status; status takes precedence and retains its original `data-status`. Metadata is not modified. Consumers can remove adapters that only copied these fields into `tags`.
|
|
158
|
+
|
|
159
|
+
### Node parsing and CI checks
|
|
160
|
+
|
|
161
|
+
Use the Node-only `storybook-addon-md/node` export. It shares discovery's parser and story resolution without loading Storybook or browser code:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { readMarkdown, parseMarkdown, resolveStoryAssociations } from 'storybook-addon-md/node';
|
|
165
|
+
|
|
166
|
+
const root = process.cwd();
|
|
167
|
+
const document = await readMarkdown('src/Button.metadata.md', root);
|
|
168
|
+
|
|
169
|
+
if (!document.body.includes('## When to use')) {
|
|
170
|
+
throw new Error(`${document.file}: missing When to use section`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const parsed = parseMarkdown('---\ntags: [Actions]\n---\n## Overview', 'virtual.md');
|
|
174
|
+
const stories = await resolveStoryAssociations(document.file, document.metadata, root);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`readMarkdown(file, root)` accepts a root-relative or absolute `.md` path and returns `{ file, original, body, metadata, stories }`. `file` and resolved `stories` are absolute paths. `original` is the complete unchanged source; `body` excludes frontmatter and normalizes BOM/CRLF just as discovery does.
|
|
178
|
+
|
|
179
|
+
`parseMarkdown(text, source)` returns `{ body, metadata }` and validates YAML, lowercase keys, title, tags, status, and story-reference value shapes. `source` labels errors. `resolveStoryAssociations(file, metadata, root)` takes parsed metadata and absolute paths, checks relative references, the four supported story extensions, file existence and root boundaries, and missing or ambiguous `.metadata.md` siblings. Explicit references take precedence. These functions throw source-specific errors; consumers can add their own template checks using `body` and metadata.
|
|
180
|
+
|
|
181
|
+
This is not a separate validation framework. These functions do not check local assets, duplicate sidebar titles, or whether stories match the consuming Storybook's globs; discovery/build still performs the relevant integration checks. Filesystem dependencies are confined to Node entry points, never the runtime export.
|
|
182
|
+
|
|
122
183
|
## Documentation manifests and MCP
|
|
123
184
|
|
|
124
185
|
Manifest support is opt-in. Set `manifests: true` in this addon's options and enable Storybook's `features.componentsManifest`:
|
|
@@ -142,6 +203,8 @@ const config: StorybookConfig = {
|
|
|
142
203
|
};
|
|
143
204
|
```
|
|
144
205
|
|
|
206
|
+
Use MCP's `docs-list` to find IDs, then `docs-show` with a component ID (for attached guidance) or standalone documentation ID.
|
|
207
|
+
|
|
145
208
|
For MCP access, install `@storybook/addon-mcp@10.6.0` and connect your MCP client to `http://localhost:6006/mcp`. Its Get Documentation tool is named `docs-show` in 10.6.0. Omit that addon if you only need JSON manifests. It is not a dependency of `storybook-addon-md`.
|
|
146
209
|
|
|
147
210
|
With Storybook and React Vite **10.6.0**, standalone Markdown appears in `/manifests/docs.json`, and attached Markdown appears in the component's `docs` in `/manifests/components.json`. Both development and static builds include the complete original source, including frontmatter. Development updates use the existing file watcher. Shared documents appear under each associated component; multiple documents on one page are joined in discovery order with two newlines. String `description` values supply optional summaries.
|
|
@@ -172,7 +235,7 @@ See [Styling](STYLING.md) for all variables, status colors, theme switching, and
|
|
|
172
235
|
- Links to `.md` files open the original source, not a rendered Docs page. Use a Storybook URL such as `/?path=/docs/guides-introduction--docs` for page navigation.
|
|
173
236
|
- Braces and JSX-like text are treated as content. Raw HTML renders as text by default.
|
|
174
237
|
- Set Storybook’s `parameters.options.storySort` for explicit sidebar ordering. See the [example preview](https://github.com/ruijdacd/storybook-addon-md/blob/main/example/.storybook/preview.ts).
|
|
175
|
-
-
|
|
238
|
+
- Multiple development Storybooks sharing one config directory are unsupported.
|
|
176
239
|
|
|
177
240
|
## Examples and contributing
|
|
178
241
|
|
|
@@ -182,7 +245,7 @@ Install dependencies with **Nub 0.7.5** and **Node 24.11+**:
|
|
|
182
245
|
nub install
|
|
183
246
|
```
|
|
184
247
|
|
|
185
|
-
Choose either example. They share stories, Markdown, and styling, with separate Storybook configurations:
|
|
248
|
+
Choose either example. They share stories, Markdown, and styling, with separate Storybook configurations. The MCP example sets `docs.defaultName: 'Reference'` to exercise custom docs names:
|
|
186
249
|
|
|
187
250
|
| Example | Configuration | Run | Build |
|
|
188
251
|
| ----------- | ------------------------------------- | ------------------------------ | ----------------------------- |
|
package/STYLING.md
CHANGED
|
@@ -178,9 +178,9 @@ export function MarkdownRenderer(document: MarkdownDocument) {
|
|
|
178
178
|
}
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
-
`MarkdownRenderer` receives `{ markdown, metadata, source }`: processed Markdown with resolved asset URLs, preserved frontmatter, and the source path relative to the project folder.
|
|
181
|
+
`MarkdownRenderer` receives `{ markdown, metadata, source, heading? }`: processed Markdown with resolved asset URLs, preserved frontmatter, and the source path relative to the project folder.
|
|
182
182
|
|
|
183
|
-
`Layout` receives `{ documents, title, attached, children, examples }`. Render `children` and `examples` to keep documentation and native example/props blocks. `examples` is `null` for standalone pages. `title` contains the standalone sidebar title and is empty for attached pages; `DefaultLayout` uses Storybook’s `Title` block for those.
|
|
183
|
+
`Layout` receives `{ documents, title, attached, children, examples, heading, tagFields }`. A standalone leading H1 is extracted in the default pipeline and supplied as the rendered `heading` node; render it instead of your fallback title. The remaining Markdown is supplied through `children`, while source files and manifests stay intact. `tagFields` lists configured metadata fields for tag display. Render `children` and `examples` to keep documentation and native example/props blocks. `examples` is `null` for standalone pages. `title` contains the standalone sidebar title and is empty for attached pages; `DefaultLayout` uses Storybook’s `Title` block for those.
|
|
184
184
|
|
|
185
185
|
Customization paths are relative to the project folder and must stay inside it. Missing files produce source-specific errors. Styling and presentation are independent options.
|
|
186
186
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { t as MarkdownOptions } from "./index-BzM0tjFM.js";
|
|
2
|
+
//#region src/content.d.ts
|
|
3
|
+
interface ContentOptions extends MarkdownOptions {
|
|
4
|
+
root: string;
|
|
5
|
+
output: string;
|
|
6
|
+
docsName?: string;
|
|
7
|
+
}
|
|
8
|
+
interface Frontmatter extends Record<string, unknown> {
|
|
9
|
+
title?: string;
|
|
10
|
+
stories?: string | string[];
|
|
11
|
+
tags?: string[];
|
|
12
|
+
status?: string;
|
|
13
|
+
}
|
|
14
|
+
type DiscoveredDocument = Awaited<ReturnType<typeof discover>>[number];
|
|
15
|
+
declare function parseMarkdown(text: string, source: string): {
|
|
16
|
+
body: string;
|
|
17
|
+
metadata: Frontmatter;
|
|
18
|
+
};
|
|
19
|
+
declare function resolveStoryAssociations(file: string, metadata: Frontmatter, root: string): Promise<string[]>;
|
|
20
|
+
declare function discover({ root, patterns, output }: ContentOptions): Promise<{
|
|
21
|
+
markdown: string;
|
|
22
|
+
assets: {
|
|
23
|
+
file: string;
|
|
24
|
+
suffix: string;
|
|
25
|
+
token: string;
|
|
26
|
+
}[];
|
|
27
|
+
heading: string | undefined;
|
|
28
|
+
file: string;
|
|
29
|
+
original: string;
|
|
30
|
+
body: string;
|
|
31
|
+
source: string;
|
|
32
|
+
title: string;
|
|
33
|
+
metadata: Frontmatter;
|
|
34
|
+
stories: string[];
|
|
35
|
+
}[]>;
|
|
36
|
+
declare function readMarkdown(file: string, root: string): Promise<{
|
|
37
|
+
file: string;
|
|
38
|
+
original: string;
|
|
39
|
+
body: string;
|
|
40
|
+
metadata: Frontmatter;
|
|
41
|
+
stories: string[];
|
|
42
|
+
}>;
|
|
43
|
+
//#endregion
|
|
44
|
+
export { readMarkdown as a, parseMarkdown as i, DiscoveredDocument as n, resolveStoryAssociations as o, Frontmatter as r, ContentOptions as t };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { glob } from "tinyglobby";
|
|
4
|
+
import { isMap, parseDocument } from "yaml";
|
|
5
|
+
import { unified } from "unified";
|
|
6
|
+
import remarkParse from "remark-parse";
|
|
7
|
+
import remarkGfm from "remark-gfm";
|
|
8
|
+
import remarkStringify from "remark-stringify";
|
|
9
|
+
import { visit } from "unist-util-visit";
|
|
10
|
+
//#region src/content.ts
|
|
11
|
+
const markdown = unified().use(remarkParse).use(remarkGfm).use(remarkStringify);
|
|
12
|
+
const storyExtensions = [
|
|
13
|
+
"tsx",
|
|
14
|
+
"ts",
|
|
15
|
+
"jsx",
|
|
16
|
+
"js"
|
|
17
|
+
];
|
|
18
|
+
const slash = (value) => value.split(path.sep).join("/");
|
|
19
|
+
const fail = (source, message) => /* @__PURE__ */ new Error(`[storybook-addon-md] ${source}: ${message}`);
|
|
20
|
+
async function localFile(file, root, source, kind) {
|
|
21
|
+
try {
|
|
22
|
+
const actual = await realpath(file);
|
|
23
|
+
const relative = path.relative(await realpath(root), actual);
|
|
24
|
+
if (relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative)) throw fail(source, `${kind} must be inside root: ${file}`);
|
|
25
|
+
if (!(await stat(actual)).isFile()) throw new Error("not a file");
|
|
26
|
+
return file;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error instanceof Error && error.message.startsWith("[storybook-addon-md]")) throw error;
|
|
29
|
+
throw Object.assign(fail(source, `missing ${kind}: ${file}`), { dependency: file });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function parseMarkdown(text, source) {
|
|
33
|
+
const normalized = text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
34
|
+
if (!normalized.startsWith("---\n")) return {
|
|
35
|
+
body: normalized,
|
|
36
|
+
metadata: {}
|
|
37
|
+
};
|
|
38
|
+
const end = /^---\s*$/gm;
|
|
39
|
+
end.lastIndex = 4;
|
|
40
|
+
const closing = end.exec(normalized);
|
|
41
|
+
if (!closing) throw fail(source, "frontmatter must end with ---");
|
|
42
|
+
const yaml = parseDocument(normalized.slice(4, closing.index), { uniqueKeys: true });
|
|
43
|
+
if (yaml.errors.length) throw fail(source, `invalid frontmatter: ${yaml.errors[0].message}`);
|
|
44
|
+
if (yaml.warnings.length) throw fail(source, `invalid frontmatter: ${yaml.warnings[0].message}`);
|
|
45
|
+
if (yaml.contents && !isMap(yaml.contents)) throw fail(source, "frontmatter must be a mapping");
|
|
46
|
+
let metadata;
|
|
47
|
+
try {
|
|
48
|
+
metadata = yaml.toJS({ maxAliasCount: 50 }) ?? {};
|
|
49
|
+
JSON.stringify(metadata, (_, value) => {
|
|
50
|
+
if (typeof value === "number" && !Number.isFinite(value)) throw new Error("metadata numbers must be finite");
|
|
51
|
+
return value;
|
|
52
|
+
});
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw fail(source, `invalid frontmatter: ${error instanceof Error ? error.message : String(error)}`);
|
|
55
|
+
}
|
|
56
|
+
if (typeof metadata !== "object" || Array.isArray(metadata)) throw fail(source, "frontmatter must be a mapping");
|
|
57
|
+
for (const key of Object.keys(metadata)) if (!/^[a-z][a-z0-9_-]*$/.test(key)) throw fail(source, `frontmatter keys must be lowercase: ${key}`);
|
|
58
|
+
if ("title" in metadata && (typeof metadata.title !== "string" || !metadata.title.trim())) throw fail(source, "title must be a non-empty string");
|
|
59
|
+
if ("status" in metadata && (typeof metadata.status !== "string" || !metadata.status.trim())) throw fail(source, "status must be a non-empty string");
|
|
60
|
+
if ("tags" in metadata && (!Array.isArray(metadata.tags) || metadata.tags.some((tag) => typeof tag !== "string" || !tag.trim()))) throw fail(source, "tags must be an array of non-empty strings");
|
|
61
|
+
if ("stories" in metadata) {
|
|
62
|
+
const references = Array.isArray(metadata.stories) ? metadata.stories : [metadata.stories];
|
|
63
|
+
if (!references.length || references.some((ref) => typeof ref !== "string" || !ref.trim())) throw fail(source, "stories must be a relative path or a non-empty array of relative paths");
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
body: normalized.slice(closing.index + closing[0].length).replace(/^\n/, ""),
|
|
67
|
+
metadata
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function resolveStoryAssociations(file, metadata, root) {
|
|
71
|
+
if ("stories" in metadata) {
|
|
72
|
+
const references = Array.isArray(metadata.stories) ? metadata.stories : [metadata.stories];
|
|
73
|
+
return Promise.all([...new Set(references)].map(async (ref) => {
|
|
74
|
+
if (path.isAbsolute(ref) || !/\.stories\.[jt]sx?$/.test(ref)) throw fail(file, `stories must reference a relative .stories.js, .jsx, .ts, or .tsx file: ${ref}`);
|
|
75
|
+
return localFile(path.resolve(path.dirname(file), ref), root, file, "story reference");
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
if (!file.endsWith(".metadata.md")) return [];
|
|
79
|
+
const base = file.slice(0, -12);
|
|
80
|
+
const matches = [];
|
|
81
|
+
for (const extension of storyExtensions) {
|
|
82
|
+
const candidate = `${base}.stories.${extension}`;
|
|
83
|
+
if (await stat(candidate).then((value) => value.isFile(), () => false)) matches.push(candidate);
|
|
84
|
+
}
|
|
85
|
+
if (matches.length !== 1) throw Object.assign(fail(file, matches.length ? "ambiguous sibling stories; set stories explicitly" : "missing sibling story for .metadata.md; set stories explicitly or rename the Markdown file"), { dependencies: storyExtensions.map((extension) => `${base}.stories.${extension}`) });
|
|
86
|
+
return [await localFile(matches[0], root, file, "story reference")];
|
|
87
|
+
}
|
|
88
|
+
async function resolveAssets(body, file, root, standalone = false) {
|
|
89
|
+
const tree = markdown.parse(body);
|
|
90
|
+
const nodes = [];
|
|
91
|
+
visit(tree, (node) => {
|
|
92
|
+
if (node.type === "image" || node.type === "link" || node.type === "definition") nodes.push(node);
|
|
93
|
+
});
|
|
94
|
+
const assets = [];
|
|
95
|
+
for (const node of nodes) {
|
|
96
|
+
const url = node.url;
|
|
97
|
+
if (!url || /^(?:[a-z][a-z\d+.-]*:|\/|#|\?)/i.test(url)) continue;
|
|
98
|
+
const [, pathname, suffix = ""] = /^([^?#]*)(.*)$/.exec(url);
|
|
99
|
+
let decoded;
|
|
100
|
+
try {
|
|
101
|
+
decoded = decodeURIComponent(pathname);
|
|
102
|
+
} catch {
|
|
103
|
+
throw fail(file, `invalid local URL: ${url}`);
|
|
104
|
+
}
|
|
105
|
+
const asset = await localFile(path.resolve(path.dirname(file), decoded), root, file, "local asset");
|
|
106
|
+
const token = `SBMDASSET${assets.length}END`;
|
|
107
|
+
if (body.includes(token)) throw fail(file, `reserved asset token in content: ${token}`);
|
|
108
|
+
assets.push({
|
|
109
|
+
file: asset,
|
|
110
|
+
suffix,
|
|
111
|
+
token
|
|
112
|
+
});
|
|
113
|
+
node.url = token;
|
|
114
|
+
}
|
|
115
|
+
const first = tree.children[0];
|
|
116
|
+
const heading = standalone && first?.type === "heading" && first.depth === 1 ? markdown.stringify({
|
|
117
|
+
...tree,
|
|
118
|
+
children: [first, ...tree.children.filter((node) => node.type === "definition")]
|
|
119
|
+
}) : void 0;
|
|
120
|
+
if (heading) tree.children.shift();
|
|
121
|
+
return {
|
|
122
|
+
markdown: markdown.stringify(tree),
|
|
123
|
+
assets,
|
|
124
|
+
heading
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function discover({ root, patterns, output }) {
|
|
128
|
+
if (!Array.isArray(patterns) || !patterns.length || patterns.some((item) => typeof item !== "string" || !item || path.isAbsolute(item) || item.split("/").includes(".."))) throw fail(root, "patterns must be a non-empty array of globs relative to root");
|
|
129
|
+
const files = await glob(patterns, {
|
|
130
|
+
cwd: root,
|
|
131
|
+
absolute: true,
|
|
132
|
+
onlyFiles: true,
|
|
133
|
+
expandDirectories: false,
|
|
134
|
+
followSymbolicLinks: false,
|
|
135
|
+
ignore: [
|
|
136
|
+
"**/node_modules/**",
|
|
137
|
+
"**/.git/**",
|
|
138
|
+
"**/storybook-static/**",
|
|
139
|
+
...path.relative(root, output).startsWith("..") ? [] : [`${slash(path.relative(root, output))}/**`]
|
|
140
|
+
]
|
|
141
|
+
});
|
|
142
|
+
return Promise.all(files.sort().filter((file) => file.endsWith(".md")).map(async (file) => {
|
|
143
|
+
const { original, body, metadata, stories } = await readMarkdown(file, root);
|
|
144
|
+
const content = await resolveAssets(body, file, root, !stories.length);
|
|
145
|
+
return {
|
|
146
|
+
file,
|
|
147
|
+
original,
|
|
148
|
+
body,
|
|
149
|
+
source: slash(path.relative(root, file)),
|
|
150
|
+
title: metadata.title ?? `Documentation/${slash(path.relative(root, file)).replace(/\.md$/, "")}`,
|
|
151
|
+
metadata,
|
|
152
|
+
stories,
|
|
153
|
+
...content
|
|
154
|
+
};
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
async function readMarkdown(file, root) {
|
|
158
|
+
root = path.resolve(root);
|
|
159
|
+
file = path.resolve(root, file);
|
|
160
|
+
if (!file.endsWith(".md")) throw fail(file, "Markdown file must have a .md extension");
|
|
161
|
+
await localFile(file, root, file, "Markdown file");
|
|
162
|
+
const original = await readFile(file, "utf8");
|
|
163
|
+
const { body, metadata } = parseMarkdown(original, file);
|
|
164
|
+
const stories = await resolveStoryAssociations(file, metadata, root);
|
|
165
|
+
return {
|
|
166
|
+
file,
|
|
167
|
+
original,
|
|
168
|
+
body,
|
|
169
|
+
metadata,
|
|
170
|
+
stories
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
export { readMarkdown as a, parseMarkdown as i, fail as n, resolveStoryAssociations as o, localFile as r, slash as s, discover as t };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
interface MarkdownOptions {
|
|
3
|
+
tagFields?: string[];
|
|
4
|
+
manifests?: boolean;
|
|
5
|
+
generatedDir?: string;
|
|
6
|
+
patterns: string[];
|
|
7
|
+
stylesheet?: string;
|
|
8
|
+
root?: string;
|
|
9
|
+
presentation?: string;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { MarkdownOptions as t };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
manifests?: boolean;
|
|
4
|
-
generatedDir?: string;
|
|
5
|
-
patterns: string[];
|
|
6
|
-
stylesheet?: string;
|
|
7
|
-
root?: string;
|
|
8
|
-
presentation?: string;
|
|
9
|
-
}
|
|
10
|
-
//#endregion
|
|
1
|
+
import { t as MarkdownOptions } from "./index-BzM0tjFM.js";
|
|
2
|
+
export { MarkdownOptions };
|
package/dist/node.d.ts
ADDED
package/dist/node.js
ADDED
package/dist/preset.d.ts
CHANGED
|
@@ -1,36 +1,15 @@
|
|
|
1
|
-
import { MarkdownOptions } from "./index.js";
|
|
1
|
+
import { t as MarkdownOptions } from "./index-BzM0tjFM.js";
|
|
2
|
+
import { n as DiscoveredDocument, t as ContentOptions } from "./content-CKQsa_HK.js";
|
|
2
3
|
import { StorybookConfigRaw } from "storybook/internal/types";
|
|
3
4
|
import { UserConfig } from "vite";
|
|
4
|
-
//#region src/content.d.ts
|
|
5
|
-
interface ContentOptions extends MarkdownOptions {
|
|
6
|
-
root: string;
|
|
7
|
-
output: string;
|
|
8
|
-
}
|
|
9
|
-
interface Frontmatter extends Record<string, unknown> {
|
|
10
|
-
title?: string;
|
|
11
|
-
stories?: string | string[];
|
|
12
|
-
tags?: string[];
|
|
13
|
-
status?: string;
|
|
14
|
-
}
|
|
15
|
-
type DiscoveredDocument = Awaited<ReturnType<typeof discover>>[number];
|
|
16
|
-
declare function discover({ root, patterns, output }: ContentOptions): Promise<{
|
|
17
|
-
markdown: string;
|
|
18
|
-
assets: {
|
|
19
|
-
file: string;
|
|
20
|
-
suffix: string;
|
|
21
|
-
token: string;
|
|
22
|
-
}[];
|
|
23
|
-
file: string;
|
|
24
|
-
original: string;
|
|
25
|
-
source: string;
|
|
26
|
-
title: string;
|
|
27
|
-
metadata: Frontmatter;
|
|
28
|
-
stories: string[];
|
|
29
|
-
}[]>;
|
|
30
|
-
//#endregion
|
|
31
5
|
//#region src/preset.d.ts
|
|
32
6
|
type PresetOptions = MarkdownOptions & {
|
|
33
7
|
configDir: string;
|
|
8
|
+
presets?: {
|
|
9
|
+
apply: (name: 'docs') => Promise<{
|
|
10
|
+
defaultName?: string;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
34
13
|
};
|
|
35
14
|
type StoryEntry = string | {
|
|
36
15
|
directory: string;
|
package/dist/preset.js
CHANGED
|
@@ -1,158 +1,9 @@
|
|
|
1
|
+
import { n as fail, r as localFile, s as slash, t as discover } from "./content-CTcZAfoF.js";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { watch } from "chokidar";
|
|
4
5
|
import picomatch from "picomatch";
|
|
5
|
-
import { mkdir, readFile, readdir,
|
|
6
|
-
import { glob } from "tinyglobby";
|
|
7
|
-
import { isMap, parseDocument } from "yaml";
|
|
8
|
-
import { unified } from "unified";
|
|
9
|
-
import remarkParse from "remark-parse";
|
|
10
|
-
import remarkGfm from "remark-gfm";
|
|
11
|
-
import remarkStringify from "remark-stringify";
|
|
12
|
-
import { visit } from "unist-util-visit";
|
|
13
|
-
//#region src/content.ts
|
|
14
|
-
const markdown = unified().use(remarkParse).use(remarkGfm).use(remarkStringify);
|
|
15
|
-
const storyExtensions = [
|
|
16
|
-
"tsx",
|
|
17
|
-
"ts",
|
|
18
|
-
"jsx",
|
|
19
|
-
"js"
|
|
20
|
-
];
|
|
21
|
-
const slash = (value) => value.split(path.sep).join("/");
|
|
22
|
-
const fail = (source, message) => /* @__PURE__ */ new Error(`[storybook-addon-md] ${source}: ${message}`);
|
|
23
|
-
async function localFile(file, root, source, kind) {
|
|
24
|
-
try {
|
|
25
|
-
const actual = await realpath(file);
|
|
26
|
-
const relative = path.relative(await realpath(root), actual);
|
|
27
|
-
if (relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative)) throw fail(source, `${kind} must be inside root: ${file}`);
|
|
28
|
-
if (!(await stat(actual)).isFile()) throw new Error("not a file");
|
|
29
|
-
return file;
|
|
30
|
-
} catch (error) {
|
|
31
|
-
if (error instanceof Error && error.message.startsWith("[storybook-addon-md]")) throw error;
|
|
32
|
-
throw Object.assign(fail(source, `missing ${kind}: ${file}`), { dependency: file });
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function parseMarkdown(text, source) {
|
|
36
|
-
const normalized = text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
37
|
-
if (!normalized.startsWith("---\n")) return {
|
|
38
|
-
body: normalized,
|
|
39
|
-
metadata: {}
|
|
40
|
-
};
|
|
41
|
-
const end = /^---\s*$/gm;
|
|
42
|
-
end.lastIndex = 4;
|
|
43
|
-
const closing = end.exec(normalized);
|
|
44
|
-
if (!closing) throw fail(source, "frontmatter must end with ---");
|
|
45
|
-
const yaml = parseDocument(normalized.slice(4, closing.index), { uniqueKeys: true });
|
|
46
|
-
if (yaml.errors.length) throw fail(source, `invalid frontmatter: ${yaml.errors[0].message}`);
|
|
47
|
-
if (yaml.warnings.length) throw fail(source, `invalid frontmatter: ${yaml.warnings[0].message}`);
|
|
48
|
-
if (yaml.contents && !isMap(yaml.contents)) throw fail(source, "frontmatter must be a mapping");
|
|
49
|
-
let metadata;
|
|
50
|
-
try {
|
|
51
|
-
metadata = yaml.toJS({ maxAliasCount: 50 }) ?? {};
|
|
52
|
-
JSON.stringify(metadata, (_, value) => {
|
|
53
|
-
if (typeof value === "number" && !Number.isFinite(value)) throw new Error("metadata numbers must be finite");
|
|
54
|
-
return value;
|
|
55
|
-
});
|
|
56
|
-
} catch (error) {
|
|
57
|
-
throw fail(source, `invalid frontmatter: ${error instanceof Error ? error.message : String(error)}`);
|
|
58
|
-
}
|
|
59
|
-
if (typeof metadata !== "object" || Array.isArray(metadata)) throw fail(source, "frontmatter must be a mapping");
|
|
60
|
-
for (const key of Object.keys(metadata)) if (!/^[a-z][a-z0-9_-]*$/.test(key)) throw fail(source, `frontmatter keys must be lowercase: ${key}`);
|
|
61
|
-
if ("title" in metadata && (typeof metadata.title !== "string" || !metadata.title.trim())) throw fail(source, "title must be a non-empty string");
|
|
62
|
-
if ("status" in metadata && (typeof metadata.status !== "string" || !metadata.status.trim())) throw fail(source, "status must be a non-empty string");
|
|
63
|
-
if ("tags" in metadata && (!Array.isArray(metadata.tags) || metadata.tags.some((tag) => typeof tag !== "string" || !tag.trim()))) throw fail(source, "tags must be an array of non-empty strings");
|
|
64
|
-
if ("stories" in metadata) {
|
|
65
|
-
const references = Array.isArray(metadata.stories) ? metadata.stories : [metadata.stories];
|
|
66
|
-
if (!references.length || references.some((ref) => typeof ref !== "string" || !ref.trim())) throw fail(source, "stories must be a relative path or a non-empty array of relative paths");
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
body: normalized.slice(closing.index + closing[0].length).replace(/^\n/, ""),
|
|
70
|
-
metadata
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
async function associations(file, metadata, root) {
|
|
74
|
-
if ("stories" in metadata) {
|
|
75
|
-
const references = Array.isArray(metadata.stories) ? metadata.stories : [metadata.stories];
|
|
76
|
-
return Promise.all([...new Set(references)].map(async (ref) => {
|
|
77
|
-
if (path.isAbsolute(ref) || !/\.stories\.[jt]sx?$/.test(ref)) throw fail(file, `stories must reference a relative .stories.js, .jsx, .ts, or .tsx file: ${ref}`);
|
|
78
|
-
return localFile(path.resolve(path.dirname(file), ref), root, file, "story reference");
|
|
79
|
-
}));
|
|
80
|
-
}
|
|
81
|
-
if (!file.endsWith(".metadata.md")) return [];
|
|
82
|
-
const base = file.slice(0, -12);
|
|
83
|
-
const matches = [];
|
|
84
|
-
for (const extension of storyExtensions) {
|
|
85
|
-
const candidate = `${base}.stories.${extension}`;
|
|
86
|
-
if (await stat(candidate).then((value) => value.isFile(), () => false)) matches.push(candidate);
|
|
87
|
-
}
|
|
88
|
-
if (matches.length !== 1) throw Object.assign(fail(file, matches.length ? "ambiguous sibling stories; set stories explicitly" : "missing sibling story for .metadata.md; set stories explicitly or rename the Markdown file"), { dependencies: storyExtensions.map((extension) => `${base}.stories.${extension}`) });
|
|
89
|
-
return [await localFile(matches[0], root, file, "story reference")];
|
|
90
|
-
}
|
|
91
|
-
async function resolveAssets(body, file, root) {
|
|
92
|
-
const tree = markdown.parse(body);
|
|
93
|
-
const nodes = [];
|
|
94
|
-
visit(tree, (node) => {
|
|
95
|
-
if (node.type === "image" || node.type === "link" || node.type === "definition") nodes.push(node);
|
|
96
|
-
});
|
|
97
|
-
const assets = [];
|
|
98
|
-
for (const node of nodes) {
|
|
99
|
-
const url = node.url;
|
|
100
|
-
if (!url || /^(?:[a-z][a-z\d+.-]*:|\/|#|\?)/i.test(url)) continue;
|
|
101
|
-
const [, pathname, suffix = ""] = /^([^?#]*)(.*)$/.exec(url);
|
|
102
|
-
let decoded;
|
|
103
|
-
try {
|
|
104
|
-
decoded = decodeURIComponent(pathname);
|
|
105
|
-
} catch {
|
|
106
|
-
throw fail(file, `invalid local URL: ${url}`);
|
|
107
|
-
}
|
|
108
|
-
const asset = await localFile(path.resolve(path.dirname(file), decoded), root, file, "local asset");
|
|
109
|
-
const token = `SBMDASSET${assets.length}END`;
|
|
110
|
-
if (body.includes(token)) throw fail(file, `reserved asset token in content: ${token}`);
|
|
111
|
-
assets.push({
|
|
112
|
-
file: asset,
|
|
113
|
-
suffix,
|
|
114
|
-
token
|
|
115
|
-
});
|
|
116
|
-
node.url = token;
|
|
117
|
-
}
|
|
118
|
-
return {
|
|
119
|
-
markdown: markdown.stringify(tree),
|
|
120
|
-
assets
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
async function discover({ root, patterns, output }) {
|
|
124
|
-
if (!Array.isArray(patterns) || !patterns.length || patterns.some((item) => typeof item !== "string" || !item || path.isAbsolute(item) || item.split("/").includes(".."))) throw fail(root, "patterns must be a non-empty array of globs relative to root");
|
|
125
|
-
const files = await glob(patterns, {
|
|
126
|
-
cwd: root,
|
|
127
|
-
absolute: true,
|
|
128
|
-
onlyFiles: true,
|
|
129
|
-
expandDirectories: false,
|
|
130
|
-
followSymbolicLinks: false,
|
|
131
|
-
ignore: [
|
|
132
|
-
"**/node_modules/**",
|
|
133
|
-
"**/.git/**",
|
|
134
|
-
"**/storybook-static/**",
|
|
135
|
-
`${slash(path.relative(root, output))}/**`
|
|
136
|
-
]
|
|
137
|
-
});
|
|
138
|
-
return Promise.all(files.sort().filter((file) => file.endsWith(".md")).map(async (file) => {
|
|
139
|
-
await localFile(file, root, file, "Markdown file");
|
|
140
|
-
const original = await readFile(file, "utf8");
|
|
141
|
-
const { body, metadata } = parseMarkdown(original, file);
|
|
142
|
-
const stories = await associations(file, metadata, root);
|
|
143
|
-
const content = await resolveAssets(body, file, root);
|
|
144
|
-
return {
|
|
145
|
-
file,
|
|
146
|
-
original,
|
|
147
|
-
source: slash(path.relative(root, file)),
|
|
148
|
-
title: metadata.title ?? `Documentation/${slash(path.relative(root, file)).replace(/\.md$/, "")}`,
|
|
149
|
-
metadata,
|
|
150
|
-
stories,
|
|
151
|
-
...content
|
|
152
|
-
};
|
|
153
|
-
}));
|
|
154
|
-
}
|
|
155
|
-
//#endregion
|
|
6
|
+
import { mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
156
7
|
//#region src/generator.ts
|
|
157
8
|
const attribute = (value) => value.replace(/[&"<>\r\n]/g, (character) => `&#${character.charCodeAt(0)};`);
|
|
158
9
|
const id = (value) => createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
@@ -171,8 +22,8 @@ async function writeChanged(file, content) {
|
|
|
171
22
|
}
|
|
172
23
|
function documentModule(document, output) {
|
|
173
24
|
const imports = document.assets.map((asset, index) => `import asset${index} from ${JSON.stringify(`${specifier(output, assetPath(asset.file, output))}?url&no-inline`)};`);
|
|
174
|
-
const expression = document.assets.reduce((value, asset, index) => `${value}.split(${JSON.stringify(asset.token)}).join(asset${index}.replace('?no-inline', '') + ${JSON.stringify(asset.suffix)})`, JSON.stringify(
|
|
175
|
-
return `${imports.join("\n")}\nexport default { source: ${JSON.stringify(document.source)}, metadata: JSON.parse(${JSON.stringify(JSON.stringify(document.metadata))}), markdown: ${expression} };\n`;
|
|
25
|
+
const expression = (markdown) => document.assets.reduce((value, asset, index) => `${value}.split(${JSON.stringify(asset.token)}).join(asset${index}.replace('?no-inline', '') + ${JSON.stringify(asset.suffix)})`, JSON.stringify(markdown));
|
|
26
|
+
return `${imports.join("\n")}\nexport default { source: ${JSON.stringify(document.source)}, metadata: JSON.parse(${JSON.stringify(JSON.stringify(document.metadata))}), markdown: ${expression(document.markdown)}, heading: ${document.heading ? expression(document.heading) : "undefined"} };\n`;
|
|
176
27
|
}
|
|
177
28
|
async function generate(options) {
|
|
178
29
|
const { output, root, presentation, stylesheet } = options;
|
|
@@ -219,8 +70,8 @@ async function generate(options) {
|
|
|
219
70
|
if (group.story) imports.push(`import * as ComponentStories from ${JSON.stringify(specifier(output, group.story))};`);
|
|
220
71
|
if (presentation) imports.push(`import * as presentation from ${JSON.stringify(specifier(output, presentation))};`);
|
|
221
72
|
if (stylesheet) imports.push(`import ${JSON.stringify(specifier(output, stylesheet))};`);
|
|
222
|
-
const meta = group.story ?
|
|
223
|
-
files.set(pageFile(key), `${imports.join("\n")}\n\n${meta}\n\n<Documentation documents={[${group.documents.map((_, index) => `document${index}`).join(", ")}]} attached={${Boolean(group.story)}} title={${JSON.stringify(group.title ?? "")}}${presentation ? " presentation={presentation}" : ""} />\n`);
|
|
73
|
+
const meta = group.story ? `<Meta of={ComponentStories} name="${attribute(options.docsName ?? "Docs")}" />` : `<Meta title="${attribute(group.title)}" />`;
|
|
74
|
+
files.set(pageFile(key), `${imports.join("\n")}\n\n${meta}\n\n<Documentation documents={[${group.documents.map((_, index) => `document${index}`).join(", ")}]} attached={${Boolean(group.story)}} title={${JSON.stringify(group.title ?? "")}} tagFields={${JSON.stringify(options.tagFields ?? [])}}${presentation ? " presentation={presentation}" : ""} />\n`);
|
|
224
75
|
}
|
|
225
76
|
files.set("status.js", "export {};\n");
|
|
226
77
|
await mkdir(output, { recursive: true });
|
|
@@ -281,12 +132,14 @@ function settings(options) {
|
|
|
281
132
|
const configDir = path.resolve(options.configDir);
|
|
282
133
|
const root = path.resolve(configDir, options.root ?? "..");
|
|
283
134
|
const generatedDir = options.generatedDir ?? "storybook-markdown-generated";
|
|
135
|
+
if (options.tagFields !== void 0 && (!Array.isArray(options.tagFields) || options.tagFields.some((field) => typeof field !== "string" || !/^[a-z][a-z0-9_-]*$/.test(field)))) throw fail(configDir, "tagFields must be an array of lowercase frontmatter field names");
|
|
284
136
|
if ("exclude" in options) throw fail(configDir, "exclude has been removed; use negative globs in patterns, such as !docs/private/**");
|
|
285
137
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(generatedDir) || ["node_modules", "storybook-static"].includes(generatedDir)) throw fail(configDir, "generatedDir must be a visible folder name containing only letters, digits, hyphens or underscores");
|
|
286
138
|
return {
|
|
287
139
|
root,
|
|
288
140
|
output: path.join(process.cwd(), generatedDir, createHash("sha256").update(configDir).digest("hex").slice(0, 12)),
|
|
289
141
|
patterns: options.patterns,
|
|
142
|
+
tagFields: options.tagFields,
|
|
290
143
|
stylesheet: options.stylesheet ? path.resolve(root, options.stylesheet) : void 0,
|
|
291
144
|
presentation: options.presentation ? path.resolve(root, options.presentation) : void 0
|
|
292
145
|
};
|
|
@@ -294,7 +147,11 @@ function settings(options) {
|
|
|
294
147
|
async function stories(existing = [], options) {
|
|
295
148
|
const key = path.resolve(options.configDir);
|
|
296
149
|
if (!sessions.has(key)) {
|
|
297
|
-
const
|
|
150
|
+
const docs = await options.presets?.apply("docs");
|
|
151
|
+
const config = {
|
|
152
|
+
...settings(options),
|
|
153
|
+
docsName: docs?.defaultName ?? "Docs"
|
|
154
|
+
};
|
|
298
155
|
sessions.set(key, {
|
|
299
156
|
config,
|
|
300
157
|
ready: generate(config)
|
package/dist/runtime.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { ComponentType, ReactNode } from "react";
|
|
|
3
3
|
interface MarkdownDocument {
|
|
4
4
|
source: string;
|
|
5
5
|
markdown: string;
|
|
6
|
+
heading?: string;
|
|
6
7
|
metadata: Record<string, unknown>;
|
|
7
8
|
}
|
|
8
9
|
interface LayoutProps {
|
|
@@ -11,6 +12,8 @@ interface LayoutProps {
|
|
|
11
12
|
attached: boolean;
|
|
12
13
|
children: ReactNode;
|
|
13
14
|
examples: ReactNode;
|
|
15
|
+
heading?: ReactNode;
|
|
16
|
+
tagFields?: string[];
|
|
14
17
|
}
|
|
15
18
|
interface Presentation {
|
|
16
19
|
Layout?: ComponentType<LayoutProps>;
|
|
@@ -19,12 +22,13 @@ interface Presentation {
|
|
|
19
22
|
//#endregion
|
|
20
23
|
//#region src/runtime.d.ts
|
|
21
24
|
export declare function DefaultMarkdownRenderer({ markdown }: MarkdownDocument): import("react").JSX.Element;
|
|
22
|
-
export declare function DefaultLayout({ documents, title, attached, children, examples }: LayoutProps): import("react").JSX.Element;
|
|
23
|
-
export declare function Documentation({ documents, title, attached, presentation }: {
|
|
25
|
+
export declare function DefaultLayout({ documents, title, attached, children, examples, heading, tagFields }: LayoutProps): import("react").JSX.Element;
|
|
26
|
+
export declare function Documentation({ documents, title, attached, presentation, tagFields }: {
|
|
24
27
|
documents: MarkdownDocument[];
|
|
25
28
|
title: string;
|
|
26
29
|
attached?: boolean;
|
|
27
30
|
presentation?: Presentation;
|
|
31
|
+
tagFields?: string[];
|
|
28
32
|
}): import("react").JSX.Element;
|
|
29
33
|
//#endregion
|
|
30
34
|
export type { LayoutProps, MarkdownDocument, Presentation };
|
package/dist/runtime.js
CHANGED
|
@@ -15,18 +15,21 @@ function DefaultMarkdownRenderer({ markdown }) {
|
|
|
15
15
|
children: markdown
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
-
function DefaultLayout({ documents, title, attached, children, examples }) {
|
|
19
|
-
const tags = [...new Set(documents.flatMap(({ metadata }) =>
|
|
18
|
+
function DefaultLayout({ documents, title, attached, children, examples, heading, tagFields = [] }) {
|
|
19
|
+
const tags = [...new Set(documents.flatMap(({ metadata }) => ["tags", ...tagFields].flatMap((field) => {
|
|
20
|
+
const value = metadata[field];
|
|
21
|
+
return (Array.isArray(value) ? value : [value]).filter((item) => typeof item === "string" && Boolean(item.trim()));
|
|
22
|
+
})))];
|
|
20
23
|
const statuses = [...new Set(documents.flatMap(({ metadata }) => typeof metadata.status === "string" ? [metadata.status] : []))];
|
|
21
24
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
22
25
|
/* @__PURE__ */ jsx("div", {
|
|
23
26
|
className: "storybook-addon-md-title",
|
|
24
|
-
children: attached ? /* @__PURE__ */ jsx(Title, {}) : /* @__PURE__ */ jsx("h1", { children: title.split("/").at(-1) })
|
|
27
|
+
children: attached ? /* @__PURE__ */ jsx(Title, {}) : heading ?? /* @__PURE__ */ jsx("h1", { children: title.split("/").at(-1) })
|
|
25
28
|
}),
|
|
26
29
|
(tags.length > 0 || statuses.length > 0) && /* @__PURE__ */ jsxs("ul", {
|
|
27
30
|
className: "storybook-addon-md-tags",
|
|
28
31
|
"aria-label": "Documentation tags",
|
|
29
|
-
children: [tags.map((tag) => /* @__PURE__ */ jsx("li", {
|
|
32
|
+
children: [tags.filter((tag) => !statuses.includes(tag)).map((tag) => /* @__PURE__ */ jsx("li", {
|
|
30
33
|
className: "storybook-addon-md-tag",
|
|
31
34
|
children: tag
|
|
32
35
|
}, `tag:${tag}`)), statuses.map((status) => /* @__PURE__ */ jsx("li", {
|
|
@@ -39,7 +42,7 @@ function DefaultLayout({ documents, title, attached, children, examples }) {
|
|
|
39
42
|
examples
|
|
40
43
|
] });
|
|
41
44
|
}
|
|
42
|
-
function Documentation({ documents, title, attached = false, presentation = {} }) {
|
|
45
|
+
function Documentation({ documents, title, attached = false, presentation = {}, tagFields = [] }) {
|
|
43
46
|
const theme = useTheme();
|
|
44
47
|
const defaults = {
|
|
45
48
|
"--sbmd-native-color": theme.color.defaultText,
|
|
@@ -51,6 +54,11 @@ function Documentation({ documents, title, attached = false, presentation = {} }
|
|
|
51
54
|
};
|
|
52
55
|
const Layout = presentation.Layout ?? DefaultLayout;
|
|
53
56
|
const Renderer = presentation.MarkdownRenderer ?? DefaultMarkdownRenderer;
|
|
57
|
+
const first = documents[0];
|
|
58
|
+
const heading = !attached && first?.heading ? /* @__PURE__ */ jsx(Renderer, {
|
|
59
|
+
...first,
|
|
60
|
+
markdown: first.heading
|
|
61
|
+
}) : void 0;
|
|
54
62
|
return /* @__PURE__ */ jsx("div", {
|
|
55
63
|
className: "storybook-addon-md-page",
|
|
56
64
|
style: defaults,
|
|
@@ -63,6 +71,8 @@ function Documentation({ documents, title, attached = false, presentation = {} }
|
|
|
63
71
|
/* @__PURE__ */ jsx(Controls, {}),
|
|
64
72
|
/* @__PURE__ */ jsx(Stories, { includePrimary: false })
|
|
65
73
|
] }) : null,
|
|
74
|
+
heading,
|
|
75
|
+
tagFields,
|
|
66
76
|
children: documents.map((document) => /* @__PURE__ */ jsx("div", {
|
|
67
77
|
className: "storybook-addon-md",
|
|
68
78
|
children: /* @__PURE__ */ jsx(Renderer, { ...document })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "storybook-addon-md",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Discover ordinary Markdown and render it in Storybook Docs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"docs",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"types": "./dist/runtime.d.ts",
|
|
37
37
|
"default": "./dist/runtime.js"
|
|
38
38
|
},
|
|
39
|
+
"./node": {
|
|
40
|
+
"types": "./dist/node.d.ts",
|
|
41
|
+
"node": "./dist/node.js"
|
|
42
|
+
},
|
|
39
43
|
"./package.json": "./package.json",
|
|
40
44
|
"./styles.css": "./dist/styles.css"
|
|
41
45
|
},
|