Merge pull request #20796 from storybookjs/markdown-block

Docs: New Markdown block
This commit is contained in:
Jeppe Reinhold 2023-01-28 00:10:10 +01:00 committed by GitHub
commit 27958ddfb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 173 additions and 113 deletions

View File

@ -547,14 +547,15 @@ This will create a `.babelrc.json` file. This file includes a bunch of babel plu
The `transcludeMarkdown` option in `addon-docs` have been removed, and the automatic handling of `.md` files in Vite projects have also been disabled.
Instead `.md` files can be imported as plain strings by adding the `?raw` suffix to the import. In an MDX file that would look like this:
Instead `.md` files can be imported as plain strings by adding the `?raw` suffix to the import, and then passed to the new `Markdown` block. In an MDX file that would look like this:
```
import { Markdown } from '@storybook/blocks';
import ReadMe from './README.md?raw';
...
{ReadMe}
<Markdown>{ReadMe}</Markdown>
```
@ -858,6 +859,20 @@ The props have been simplified and the block now only accepts an `of` prop, whic
`parameters.notes` and `parameters.info` have been deprecated as a way to specify descriptions. Instead use JSDoc comments above the default export or story export, or use `parameters.docs.description.story | component` directly. See TDB DOCS LINK for a deeper explanation on how to write descriptions.
If you were previously using the `Description` block to render plain markdown in your docs, that behavior can now be achieved with the new `Markdown` block instead like this:
```
import { Markdown } from '@storybook/blocks';
import ReadMe from './README.md?raw';
...
<Markdown>{ReadMe}</Markdown>
```
Notice the `?raw` suffix in the markdown import is needed for this to work.
##### Story block
To reference a story in a MDX file, you should reference it with `of`:
@ -1017,8 +1032,8 @@ Then enable the `legacyMdx1` feature flag in your `.storybook/main.js` file:
export default {
features: {
legacyMdx1: true,
}
}
},
};
```
NOTE: This only affects `.(stories|story).mdx` files. Notably, if you want to use Storybook 7's "pure" `.mdx` format, you'll need to use MDX2 for that.

View File

@ -58,7 +58,7 @@
"color-convert": "^2.0.1",
"dequal": "^2.0.2",
"lodash": "^4.17.21",
"markdown-to-jsx": "^7.1.3",
"markdown-to-jsx": "^7.1.8",
"memoizerific": "^1.11.3",
"polished": "^4.2.2",
"react-colorful": "^5.1.2",

View File

@ -2,13 +2,13 @@ import type { FC } from 'react';
import React, { useContext } from 'react';
import { str } from '@storybook/docs-tools';
import { deprecate } from '@storybook/client-logger';
import { Description } from '../components';
import type { DocsContextProps } from './DocsContext';
import { DocsContext } from './DocsContext';
import type { Component } from './types';
import type { Of } from './useOf';
import { useOf } from './useOf';
import { Markdown } from './Markdown';
export enum DescriptionType {
INFO = 'info',
@ -151,7 +151,7 @@ const DescriptionContainer: FC<DescriptionProps> = (props) => {
`The 'children' prop on the Description block is deprecated. See ${DEPRECATION_MIGRATION_LINK}`
);
}
return markdown ? <Description markdown={markdown} /> : null;
return markdown ? <Markdown>{markdown}</Markdown> : null;
};
export { DescriptionContainer as Description };

View File

@ -0,0 +1,75 @@
import dedent from 'ts-dedent';
import { Markdown as MarkdownComponent } from './Markdown';
// eslint-disable-next-line import/no-unresolved
import mdContent from '../examples/Markdown-content.md?raw';
export default {
component: MarkdownComponent,
};
export const Markdown = {
args: {
children: dedent`
# My Example Markdown
The group looked like tall, exotic grazing animals, swaying gracefully and unconsciously with the movement of the train, their high heels like polished hooves against the gray metal of the Flatline as a construct, a hardwired ROM cassette replicating a dead mans skills, obsessions, kneejerk responses.
![An image](https://storybook.js.org/images/placeholders/350x150.png)
He stared at the clinic, Molly took him to the Tank War, mouth touched with hot gold as a gliding cursor struck sparks from the wall of a skyscraper canyon.
Paragraph with an \`inline code\` block.
\`\`\`tsx
// TypeScript React code block
export const MyStory = () => {
return <Button>Click me</Button>;
};
\`\`\`
\`\`\`
code block with with no language
const a = fn({
b: 2,
});
\`\`\`
<h3>Native h3 element</h3>
# [Link](https://storybook.js.org/) in heading
## [Link](https://storybook.js.org/) in heading
### [Link](https://storybook.js.org/) in heading
#### [Link](https://storybook.js.org/) in heading
##### [Link](https://storybook.js.org/) in heading
###### [Link](https://storybook.js.org/) in heading
He stared at the clinic, [Molly](https://storybook.js.org/) took him to the *[Tank War](https://storybook.js.org/)*, mouth touched with hot gold as a gliding cursor struck sparks from the wall of a **[skyscraper](https://storybook.js.org/)** canyon.
{ brackets, valid MD but invalid MDX - works here }
<Looks like a JSX tag/>
<!-- above is valid MD but invalid in markdown-to-jsx, so it will not be rendered -->
\`<Looks like a JSX tag />\`
The above is only visible because it is wrapped in backticks
`,
},
};
/**
* The Markdown component won't know the difference between getting a raw string
* and something imported from a .md file.
* So this story doesn't actually test the component, but rather the import
* at the top of the CSF file
*/
export const ImportedMDFile = {
name: 'Imported .md file',
args: { children: mdContent },
};
export const Text = {
args: {
children: `That was Wintermute, manipulating the lock the way it had manipulated the drone micro and the amplified breathing of the room where Case waited. The semiotics of the bright void beyond the chain link. The tug Marcus Garvey, a steel drum nine meters long and two in diameter, creaked and shuddered as Maelcum punched for a California gambling cartel, then as a paid killer in the dark, curled in his capsule in some coffin hotel, his hands clawed into the nearest door and watched the other passengers as he rode. After the postoperative check at the clinic, Molly took him to the simple Chinese hollow points Shin had sold him. Still it was a handgun and nine rounds of ammunition, and as he made his way down Shiga from the missionaries, the train reached Cases station. Now this quiet courtyard, Sunday afternoon, this girl with a random collection of European furniture, as though Deane had once intended to use the place as his home. Case felt the edge of the Flatline as a construct, a hardwired ROM cassette replicating a dead mans skills, obsessions, kneejerk responses. They were dropping, losing altitude in a canyon of rainbow foliage, a lurid communal mural that completely covered the hull of the console in faded pinks and yellows.`,
},
};

View File

@ -0,0 +1,50 @@
/* eslint-disable react/destructuring-assignment */
import React from 'react';
import PureMarkdown from 'markdown-to-jsx';
import dedent from 'ts-dedent';
import { AnchorMdx, CodeOrSourceMdx, HeadersMdx } from './mdx';
// mirror props from markdown-to-jsx. From https://react-typescript-cheatsheet.netlify.app/docs/advanced/patterns_by_usecase#wrappingmirroring-a-component
type MarkdownProps = typeof PureMarkdown extends React.ComponentType<infer Props> ? Props : never;
export const Markdown = (props: MarkdownProps) => {
if (!props.children) {
return null;
}
if (typeof props.children !== 'string') {
throw new Error(
dedent`The Markdown block only accepts children as a single string, but children were of type: '${typeof props.children}'
This is often caused by not wrapping the child in a template string.
This is invalid:
<Markdown>
# Some heading
A paragraph
</Markdown>
Instead do:
<Markdown>
{\`
# Some heading
A paragraph
\`}
</Markdown>
`
);
}
return (
<PureMarkdown
{...props}
options={{
forceBlock: true,
overrides: {
code: CodeOrSourceMdx,
a: AnchorMdx,
...HeadersMdx,
...props?.options?.overrides,
},
...props?.options,
}}
/>
);
};

View File

@ -14,6 +14,7 @@ export * from './DocsStory';
export * from './external/ExternalDocs';
export * from './external/ExternalDocsContainer';
export * from './Heading';
export * from './Markdown';
export * from './Meta';
export * from './Primary';
// eslint-disable-next-line import/export

View File

@ -1,70 +0,0 @@
import { Description } from './Description';
export default {
component: Description,
parameters: { docsStyles: true },
};
const textCaption = `That was Wintermute, manipulating the lock the way it had manipulated the drone micro and the amplified breathing of the room where Case waited. The semiotics of the bright void beyond the chain link. The tug Marcus Garvey, a steel drum nine meters long and two in diameter, creaked and shuddered as Maelcum punched for a California gambling cartel, then as a paid killer in the dark, curled in his capsule in some coffin hotel, his hands clawed into the nearest door and watched the other passengers as he rode. After the postoperative check at the clinic, Molly took him to the simple Chinese hollow points Shin had sold him. Still it was a handgun and nine rounds of ammunition, and as he made his way down Shiga from the missionaries, the train reached Cases station. Now this quiet courtyard, Sunday afternoon, this girl with a random collection of European furniture, as though Deane had once intended to use the place as his home. Case felt the edge of the Flatline as a construct, a hardwired ROM cassette replicating a dead mans skills, obsessions, kneejerk responses. They were dropping, losing altitude in a canyon of rainbow foliage, a lurid communal mural that completely covered the hull of the console in faded pinks and yellows.`;
const markdownCaption = `
# My Example Markdown
The group looked like tall, exotic grazing animals, swaying gracefully and unconsciously with the movement of the train, their high heels like polished hooves against the gray metal of the Flatline as a construct, a hardwired ROM cassette replicating a dead mans skills, obsessions, kneejerk responses.
![An image](https://storybook.js.org/images/placeholders/350x150.png)
He stared at the clinic, Molly took him to the Tank War, mouth touched with hot gold as a gliding cursor struck sparks from the wall of a skyscraper canyon.
`;
const markdownWithLinksCaption = `
# [Link](https://storybook.js.org/) in heading
## [Link](https://storybook.js.org/) in heading
### [Link](https://storybook.js.org/) in heading
#### [Link](https://storybook.js.org/) in heading
##### [Link](https://storybook.js.org/) in heading
###### [Link](https://storybook.js.org/) in heading
He stared at the clinic, [Molly](https://storybook.js.org/) took him to the *[Tank War](https://storybook.js.org/)*, mouth touched with hot gold as a gliding cursor struck sparks from the wall of a **[skyscraper](https://storybook.js.org/)** canyon.
`;
const markdownWithCodeSnippets = `
# My Example Markdown
An \`inline\` codeblock
\`\`\`tsx
// TypeScript React code block
export const MyStory = () => {
return <Button>Click me</Button>;
};
\`\`\`
\`\`\`
code block with with no language
const a = fn({
b: 2,
});
\`\`\`
`;
export const Text = {
args: {
markdown: textCaption,
},
};
export const Markdown = {
args: {
markdown: markdownCaption,
},
};
export const MarkdownLinks = {
args: {
markdown: markdownWithLinksCaption,
},
};
export const MarkdownCodeSnippets = {
args: {
markdown: markdownWithCodeSnippets,
},
};

View File

@ -1,23 +0,0 @@
import type { FC } from 'react';
import React from 'react';
import Markdown from 'markdown-to-jsx';
import { components } from '@storybook/components';
export interface DescriptionProps {
markdown: string;
}
/**
* A markdown description for a component, typically used to show the
* components docgen docs.
*/
export const Description: FC<DescriptionProps> = ({ markdown }) => (
<Markdown
options={{
forceBlock: true,
overrides: { code: components.code, pre: components.pre, a: components.a },
}}
>
{markdown}
</Markdown>
);

View File

@ -2,12 +2,13 @@
import type { ComponentProps } from 'react';
import React from 'react';
import { Global, css } from '@storybook/theming';
import { Source, ArgsTable, Description } from '.';
import { Source, ArgsTable } from '.';
import { Title, Subtitle, DocsPageWrapper } from './DocsPage';
import { Markdown as MarkdownComponent } from '../blocks/Markdown';
import * as Preview from './Preview.stories';
import * as argsTable from './ArgsTable/ArgsTable.stories';
import * as source from './Source.stories';
import * as description from './Description.stories';
import * as markdown from '../blocks/Markdown.stories';
import { Unstyled } from '../blocks/Unstyled';
export default {
@ -40,7 +41,7 @@ export const Loading = () => (
<Subtitle>
What the DocsPage looks like. Meant to be QAed in Canvas tab not in Docs tab.
</Subtitle>
<Description {...description.Text.args} />
<MarkdownComponent {...markdown.Text.args} />
<Preview.Loading />
<ArgsTable {...(argsTable.Loading.args as ComponentProps<typeof ArgsTable>)} />
<Source {...source.Loading.args} />
@ -53,7 +54,7 @@ export const WithSubtitle = () => (
<Subtitle>
What the DocsPage looks like. Meant to be QAed in Canvas tab not in Docs tab.
</Subtitle>
<Description {...description.Text.args} />
<MarkdownComponent {...markdown.Text.args} />
<Preview.Single />
<ArgsTable {...argsTable.Normal.args} />
<Source {...source.JSX.args} />
@ -72,7 +73,7 @@ export const NoText = () => (
export const Text = () => (
<DocsPageWrapper>
<Title>Sensorium</Title>
<Description {...description.Text.args} />
<MarkdownComponent {...markdown.Text.args} />
<Preview.Single />
<ArgsTable {...argsTable.Normal.args} />
<Source {...source.JSX.args} />
@ -82,7 +83,7 @@ export const Text = () => (
export const Markdown = () => (
<DocsPageWrapper>
<Title>markdown</Title>
<Description {...description.Markdown.args} />
<MarkdownComponent {...markdown.Markdown.args} />
<Preview.Single />
<ArgsTable {...argsTable.Normal.args} />
<Source {...source.JSX.args} />

View File

@ -1,6 +1,5 @@
export * from './Source';
export * from './EmptyBlock';
export * from './Description';
export * from './DocsPage';
// eslint-disable-next-line import/no-cycle
export * from './Preview';

View File

@ -0,0 +1,14 @@
# This is an `.md` file
it has been imported using `import content from './Markdown-content.md?raw'`
Notice the `?raw` at the end above, it is necessary to work.
A full example:
```md
import { Markdown } from '@storybook/blocks';
import content from './Markdown-content.md?raw';
<Markdown>{content}</Markdown>
```

View File

@ -1,6 +1,5 @@
/* eslint-disable no-underscore-dangle */
/* eslint-disable @typescript-eslint/naming-convention */
declare module 'markdown-to-jsx';
declare module '*.md';
declare var __DOCS_CONTEXT__: any;

View File

@ -1,3 +1,2 @@
declare module 'markdown-to-jsx';
declare module '*.md';
declare module '*.mdx';

View File

@ -74,7 +74,7 @@
"fs-extra": "^11.1.0",
"fuse.js": "^3.6.1",
"lodash": "^4.17.21",
"markdown-to-jsx": "^7.1.3",
"markdown-to-jsx": "^7.1.8",
"memoizerific": "^1.11.3",
"polished": "^4.2.2",
"qs": "^6.10.0",

View File

@ -5702,7 +5702,7 @@ __metadata:
color-convert: ^2.0.1
dequal: ^2.0.2
lodash: ^4.17.21
markdown-to-jsx: ^7.1.3
markdown-to-jsx: ^7.1.8
memoizerific: ^1.11.3
polished: ^4.2.2
react-colorful: ^5.1.2
@ -6463,7 +6463,7 @@ __metadata:
fs-extra: ^11.1.0
fuse.js: ^3.6.1
lodash: ^4.17.21
markdown-to-jsx: ^7.1.3
markdown-to-jsx: ^7.1.8
memoizerific: ^1.11.3
polished: ^4.2.2
qs: ^6.10.0
@ -20259,7 +20259,7 @@ __metadata:
languageName: node
linkType: hard
"markdown-to-jsx@npm:^7.1.3":
"markdown-to-jsx@npm:^7.1.8":
version: 7.1.8
resolution: "markdown-to-jsx@npm:7.1.8"
peerDependencies: