storybook-addon-md 0.7.0 → 0.8.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 +11 -0
- package/README.md +26 -1
- package/dist/{content-CTcZAfoF.js → content-BrghNgvp.js} +47 -9
- package/dist/{content-CKQsa_HK.d.ts → content-CwjmOiRK.d.ts} +2 -2
- package/dist/{index-BzM0tjFM.d.ts → index-D3ynUvxB.d.ts} +6 -1
- package/dist/index.d.ts +2 -2
- package/dist/node.d.ts +1 -1
- package/dist/node.js +1 -1
- package/dist/preset.d.ts +2 -2
- package/dist/preset.js +8 -2
- package/dist/runtime.js +13 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# storybook-addon-md
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 298284c: Add an opt-in `links` option that rewrites relative Markdown links instead of bundling their targets as assets.
|
|
8
|
+
|
|
9
|
+
- `links.documents` (default `true` when `links` is set) turns links to other discovered documents into ordinary links to their Docs page in the Storybook manager.
|
|
10
|
+
- `links.repository` turns links to other files or folders inside root into `<repository>/<relative path>` links, so source files are no longer copied into the bundle and folder links no longer fail discovery.
|
|
11
|
+
|
|
12
|
+
Images, image reference definitions, manifests, and the default behavior without `links` are unchanged.
|
|
13
|
+
|
|
3
14
|
## 0.7.0
|
|
4
15
|
|
|
5
16
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -159,6 +159,7 @@ Invalid frontmatter, missing or ambiguous story references, and missing local as
|
|
|
159
159
|
| `manifests` | `false` | Include original Markdown in Storybook documentation manifests. |
|
|
160
160
|
| `tagFields` | `[]` | Additional frontmatter fields displayed as tags. |
|
|
161
161
|
| `presentation` | None | Module exporting `Layout` and/or `MarkdownRenderer`. |
|
|
162
|
+
| `links` | None | Rewrite relative links to Docs pages and repository files. |
|
|
162
163
|
|
|
163
164
|
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.
|
|
164
165
|
|
|
@@ -186,6 +187,30 @@ const config: StorybookConfig = {
|
|
|
186
187
|
|
|
187
188
|
`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`.
|
|
188
189
|
|
|
190
|
+
### Relative links
|
|
191
|
+
|
|
192
|
+
By default every relative link is bundled as an asset, so a link to another Markdown file opens its raw source. Set `links` to rewrite relative links instead:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
{
|
|
196
|
+
name: 'storybook-addon-md',
|
|
197
|
+
options: {
|
|
198
|
+
patterns: ['src/**/*.md', 'docs/**/*.md'],
|
|
199
|
+
links: {
|
|
200
|
+
documents: true,
|
|
201
|
+
repository: 'https://github.com/acme/design-system/blob/main',
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
| Field | Default | Behavior |
|
|
208
|
+
| ------------ | ------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
209
|
+
| `documents` | `true` | Links to discovered Markdown documents become ordinary links to their Docs page in the manager, opened in the top frame. |
|
|
210
|
+
| `repository` | None | Links to other files or folders inside the project folder become `<repository>/<path relative to root>` links. |
|
|
211
|
+
|
|
212
|
+
Fragments and query strings are preserved. Attached documents link to their story file's Docs page using the CSF `title`; when the story file has no explicit title, the link falls back to the `story:<path>` key and will not resolve until a title is set. Images and image reference definitions remain bundled assets, and manifests keep the original Markdown. Relative links to files outside root, or to missing files, still fail the build. Without `links`, behavior is unchanged.
|
|
213
|
+
|
|
189
214
|
### Node parsing and CI checks
|
|
190
215
|
|
|
191
216
|
Use the Node-only `storybook-addon-md/node` export. It shares discovery's parser and story resolution without loading Storybook or browser code:
|
|
@@ -262,7 +287,7 @@ See [Styling](STYLING.md) for all variables, status and callout colors, theme sw
|
|
|
262
287
|
## Links and limitations
|
|
263
288
|
|
|
264
289
|
- Relative links and images resolve from the Markdown source and are included in static builds. Root-relative assets use Storybook’s `staticDirs`.
|
|
265
|
-
-
|
|
290
|
+
- Without the `links` option, links to `.md` files open the original source, not a rendered Docs page. Set `links` or use a Storybook URL such as `?path=/docs/guides-introduction--docs` for page navigation.
|
|
266
291
|
- Braces and JSX-like text are treated as content. Raw HTML renders as text by default.
|
|
267
292
|
- 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).
|
|
268
293
|
- Multiple development Storybooks sharing one config directory are unsupported.
|
|
@@ -17,12 +17,13 @@ const storyExtensions = [
|
|
|
17
17
|
];
|
|
18
18
|
const slash = (value) => value.split(path.sep).join("/");
|
|
19
19
|
const fail = (source, message) => /* @__PURE__ */ new Error(`[storybook-addon-md] ${source}: ${message}`);
|
|
20
|
-
async function localFile(file, root, source, kind) {
|
|
20
|
+
async function localFile(file, root, source, kind, directories = false) {
|
|
21
21
|
try {
|
|
22
22
|
const actual = await realpath(file);
|
|
23
23
|
const relative = path.relative(await realpath(root), actual);
|
|
24
24
|
if (relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative)) throw fail(source, `${kind} must be inside root: ${file}`);
|
|
25
|
-
|
|
25
|
+
const info = await stat(actual);
|
|
26
|
+
if (!info.isFile() && !(directories && info.isDirectory())) throw new Error("not a file");
|
|
26
27
|
return file;
|
|
27
28
|
} catch (error) {
|
|
28
29
|
if (error instanceof Error && error.message.startsWith("[storybook-addon-md]")) throw error;
|
|
@@ -85,12 +86,20 @@ async function resolveStoryAssociations(file, metadata, root) {
|
|
|
85
86
|
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
87
|
return [await localFile(matches[0], root, file, "story reference")];
|
|
87
88
|
}
|
|
88
|
-
|
|
89
|
+
const imageDefinitions = (tree) => {
|
|
90
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
91
|
+
visit(tree, "imageReference", (node) => {
|
|
92
|
+
identifiers.add(node.identifier);
|
|
93
|
+
});
|
|
94
|
+
return identifiers;
|
|
95
|
+
};
|
|
96
|
+
async function resolveAssets(body, file, root, standalone = false, resolveLink) {
|
|
89
97
|
const tree = markdown.parse(body);
|
|
90
98
|
const nodes = [];
|
|
91
99
|
visit(tree, (node) => {
|
|
92
100
|
if (node.type === "image" || node.type === "link" || node.type === "definition") nodes.push(node);
|
|
93
101
|
});
|
|
102
|
+
const images = imageDefinitions(tree);
|
|
94
103
|
const assets = [];
|
|
95
104
|
for (const node of nodes) {
|
|
96
105
|
const url = node.url;
|
|
@@ -102,7 +111,13 @@ async function resolveAssets(body, file, root, standalone = false) {
|
|
|
102
111
|
} catch {
|
|
103
112
|
throw fail(file, `invalid local URL: ${url}`);
|
|
104
113
|
}
|
|
105
|
-
const
|
|
114
|
+
const target = path.resolve(path.dirname(file), decoded);
|
|
115
|
+
const link = node.type === "image" || node.type === "definition" && images.has(node.identifier) || !resolveLink ? void 0 : await resolveLink(target, file);
|
|
116
|
+
if (link) {
|
|
117
|
+
node.url = `${link}${suffix}`;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const asset = await localFile(target, root, file, "local asset");
|
|
106
121
|
const token = `SBMDASSET${assets.length}END`;
|
|
107
122
|
if (body.includes(token)) throw fail(file, `reserved asset token in content: ${token}`);
|
|
108
123
|
assets.push({
|
|
@@ -124,7 +139,7 @@ async function resolveAssets(body, file, root, standalone = false) {
|
|
|
124
139
|
heading
|
|
125
140
|
};
|
|
126
141
|
}
|
|
127
|
-
async function discover({ root, patterns, output }) {
|
|
142
|
+
async function discover({ root, patterns, output, links, docsName }) {
|
|
128
143
|
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
144
|
const files = await glob(patterns, {
|
|
130
145
|
cwd: root,
|
|
@@ -139,9 +154,8 @@ async function discover({ root, patterns, output }) {
|
|
|
139
154
|
...path.relative(root, output).startsWith("..") ? [] : [`${slash(path.relative(root, output))}/**`]
|
|
140
155
|
]
|
|
141
156
|
});
|
|
142
|
-
|
|
157
|
+
const documents = await Promise.all(files.sort().filter((file) => file.endsWith(".md")).map(async (file) => {
|
|
143
158
|
const { original, body, metadata, stories } = await readMarkdown(file, root);
|
|
144
|
-
const content = await resolveAssets(body, file, root, !stories.length);
|
|
145
159
|
return {
|
|
146
160
|
file,
|
|
147
161
|
original,
|
|
@@ -149,10 +163,34 @@ async function discover({ root, patterns, output }) {
|
|
|
149
163
|
source: slash(path.relative(root, file)),
|
|
150
164
|
title: metadata.title ?? `Documentation/${slash(path.relative(root, file)).replace(/\.md$/, "")}`,
|
|
151
165
|
metadata,
|
|
152
|
-
stories
|
|
153
|
-
...content
|
|
166
|
+
stories
|
|
154
167
|
};
|
|
155
168
|
}));
|
|
169
|
+
const resolveLink = links ? await linkResolver(links, documents, root, docsName) : void 0;
|
|
170
|
+
return Promise.all(documents.map(async (document) => ({
|
|
171
|
+
...document,
|
|
172
|
+
...await resolveAssets(document.body, document.file, root, !document.stories.length, resolveLink)
|
|
173
|
+
})));
|
|
174
|
+
}
|
|
175
|
+
async function docsId(document, root, docsName, titles) {
|
|
176
|
+
const { toId } = await import("storybook/internal/csf");
|
|
177
|
+
if (!document.stories.length) return toId(document.title, docsName);
|
|
178
|
+
const [story] = document.stories;
|
|
179
|
+
if (!titles.has(story)) titles.set(story, import("storybook/internal/csf-tools").then(({ readCsf }) => readCsf(story, { makeTitle: (title) => title })).then((csf) => csf.parse().meta).catch(() => void 0));
|
|
180
|
+
const meta = await titles.get(story);
|
|
181
|
+
return meta?.title ? toId(meta.id ?? meta.title, docsName) : `story:${slash(path.relative(root, story))}`;
|
|
182
|
+
}
|
|
183
|
+
async function linkResolver(links, documents, root, docsName = "Docs") {
|
|
184
|
+
const paths = /* @__PURE__ */ new Map();
|
|
185
|
+
const titles = /* @__PURE__ */ new Map();
|
|
186
|
+
if (links.documents !== false) for (const document of documents) paths.set(document.file, `?path=/docs/${await docsId(document, root, docsName, titles)}`);
|
|
187
|
+
const repository = links.repository?.replace(/\/+$/, "");
|
|
188
|
+
return async (target, source) => {
|
|
189
|
+
const page = paths.get(target);
|
|
190
|
+
if (page || !repository) return page;
|
|
191
|
+
await localFile(target, root, source, "link target", true);
|
|
192
|
+
return `${repository}/${slash(path.relative(root, target))}`;
|
|
193
|
+
};
|
|
156
194
|
}
|
|
157
195
|
async function readMarkdown(file, root) {
|
|
158
196
|
root = path.resolve(root);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as MarkdownOptions } from "./index-D3ynUvxB.js";
|
|
2
2
|
//#region src/content.d.ts
|
|
3
3
|
interface ContentOptions extends MarkdownOptions {
|
|
4
4
|
root: string;
|
|
@@ -17,7 +17,7 @@ declare function parseMarkdown(text: string, source: string): {
|
|
|
17
17
|
metadata: Frontmatter;
|
|
18
18
|
};
|
|
19
19
|
declare function resolveStoryAssociations(file: string, metadata: Frontmatter, root: string): Promise<string[]>;
|
|
20
|
-
declare function discover({ root, patterns, output }: ContentOptions): Promise<{
|
|
20
|
+
declare function discover({ root, patterns, output, links, docsName }: ContentOptions): Promise<{
|
|
21
21
|
markdown: string;
|
|
22
22
|
assets: {
|
|
23
23
|
file: string;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
//#region src/index.d.ts
|
|
2
|
+
interface LinkOptions {
|
|
3
|
+
documents?: boolean;
|
|
4
|
+
repository?: string;
|
|
5
|
+
}
|
|
2
6
|
interface MarkdownOptions {
|
|
3
7
|
tagFields?: string[];
|
|
4
8
|
manifests?: boolean;
|
|
@@ -7,6 +11,7 @@ interface MarkdownOptions {
|
|
|
7
11
|
stylesheet?: string;
|
|
8
12
|
root?: string;
|
|
9
13
|
presentation?: string;
|
|
14
|
+
links?: LinkOptions;
|
|
10
15
|
}
|
|
11
16
|
//#endregion
|
|
12
|
-
export { MarkdownOptions as t };
|
|
17
|
+
export { MarkdownOptions as n, LinkOptions as t };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { MarkdownOptions };
|
|
1
|
+
import { n as MarkdownOptions, t as LinkOptions } from "./index-D3ynUvxB.js";
|
|
2
|
+
export { LinkOptions, MarkdownOptions };
|
package/dist/node.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as readMarkdown, i as parseMarkdown, o as resolveStoryAssociations, r as Frontmatter } from "./content-
|
|
1
|
+
import { a as readMarkdown, i as parseMarkdown, o as resolveStoryAssociations, r as Frontmatter } from "./content-CwjmOiRK.js";
|
|
2
2
|
export { type Frontmatter, parseMarkdown, readMarkdown, resolveStoryAssociations };
|
package/dist/node.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as readMarkdown, i as parseMarkdown, o as resolveStoryAssociations } from "./content-
|
|
1
|
+
import { a as readMarkdown, i as parseMarkdown, o as resolveStoryAssociations } from "./content-BrghNgvp.js";
|
|
2
2
|
export { parseMarkdown, readMarkdown, resolveStoryAssociations };
|
package/dist/preset.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as DiscoveredDocument, t as ContentOptions } from "./content-
|
|
1
|
+
import { n as MarkdownOptions } from "./index-D3ynUvxB.js";
|
|
2
|
+
import { n as DiscoveredDocument, t as ContentOptions } from "./content-CwjmOiRK.js";
|
|
3
3
|
import { StorybookConfigRaw } from "storybook/internal/types";
|
|
4
4
|
import { UserConfig } from "vite";
|
|
5
5
|
//#region src/preset.d.ts
|
package/dist/preset.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as fail, r as localFile, s as slash, t as discover } from "./content-
|
|
1
|
+
import { n as fail, r as localFile, s as slash, t as discover } from "./content-BrghNgvp.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { watch } from "chokidar";
|
|
@@ -133,6 +133,8 @@ function settings(options) {
|
|
|
133
133
|
const root = path.resolve(configDir, options.root ?? "..");
|
|
134
134
|
const generatedDir = options.generatedDir ?? "storybook-markdown-generated";
|
|
135
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");
|
|
136
|
+
const { links } = options;
|
|
137
|
+
if (links !== void 0 && (typeof links !== "object" || links === null || Array.isArray(links) || Object.keys(links).some((key) => !["documents", "repository"].includes(key)) || links.documents !== void 0 && typeof links.documents !== "boolean" || links.repository !== void 0 && (typeof links.repository !== "string" || !/^https?:\/\/\S+$/.test(links.repository)))) throw fail(configDir, "links must be an object with an optional documents boolean and an optional repository URL");
|
|
136
138
|
if ("exclude" in options) throw fail(configDir, "exclude has been removed; use negative globs in patterns, such as !docs/private/**");
|
|
137
139
|
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");
|
|
138
140
|
return {
|
|
@@ -141,7 +143,11 @@ function settings(options) {
|
|
|
141
143
|
patterns: options.patterns,
|
|
142
144
|
tagFields: options.tagFields,
|
|
143
145
|
stylesheet: options.stylesheet ? path.resolve(root, options.stylesheet) : void 0,
|
|
144
|
-
presentation: options.presentation ? path.resolve(root, options.presentation) : void 0
|
|
146
|
+
presentation: options.presentation ? path.resolve(root, options.presentation) : void 0,
|
|
147
|
+
links: links ? {
|
|
148
|
+
documents: links.documents ?? true,
|
|
149
|
+
repository: links.repository?.replace(/\/+$/, "")
|
|
150
|
+
} : void 0
|
|
145
151
|
};
|
|
146
152
|
}
|
|
147
153
|
async function stories(existing = [], options) {
|
package/dist/runtime.js
CHANGED
|
@@ -34,6 +34,18 @@ function Blockquote({ children, ...props }) {
|
|
|
34
34
|
}), content]
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
+
function Anchor({ href, target, ...props }) {
|
|
38
|
+
if (!href?.startsWith("?path=")) return /* @__PURE__ */ jsx("a", {
|
|
39
|
+
...props,
|
|
40
|
+
href,
|
|
41
|
+
target
|
|
42
|
+
});
|
|
43
|
+
return /* @__PURE__ */ jsx("a", {
|
|
44
|
+
...props,
|
|
45
|
+
href: new URL(href, new URL("./", window.location.href)).href,
|
|
46
|
+
target: target ?? "_top"
|
|
47
|
+
});
|
|
48
|
+
}
|
|
37
49
|
function DefaultMarkdownRenderer({ markdown }) {
|
|
38
50
|
return /* @__PURE__ */ jsx(Markdown, {
|
|
39
51
|
options: {
|
|
@@ -41,7 +53,7 @@ function DefaultMarkdownRenderer({ markdown }) {
|
|
|
41
53
|
overrides: {
|
|
42
54
|
code: CodeOrSourceMdx,
|
|
43
55
|
...HeadersMdx,
|
|
44
|
-
a:
|
|
56
|
+
a: Anchor,
|
|
45
57
|
blockquote: Blockquote
|
|
46
58
|
}
|
|
47
59
|
},
|