Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 136 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,34 @@ function post(tree, options) {

return toJsxRuntime(tree, {
Fragment,
components,
components: {
...components,
code: ({node, className, children, ...props}) => {
const match = /language-(\w+)/.exec(className || '')

if (match && match[1] === 'mermaid') {
// We need to use a side effect to render mermaid, but this is happening in render
// This is a simplified implementation for demonstration
// In a real library, we would want to move this to a proper useEffect hook inside a component
// But since we are modifying the library to support it generally, let's wrap it in a component
return jsx(MermaidGraph, {
chart: String(children),
component: components && components.code,
node,
className,
children,
...props
})
}

// If user provided a code component, use it
if (components && components.code) {
return jsx(components.code, {node, className, children, ...props})
}

return jsx('code', {className, ...props, children})
}
},
ignoreInvalidStyle: true,
jsx,
jsxs,
Expand All @@ -371,6 +398,29 @@ function post(tree, options) {
/** @type {string} */
let key

// Unwrap `pre` for `mermaid`.
if (
node.tagName === 'pre' &&
node.children.length === 1 &&
node.children[0].type === 'element' &&
node.children[0].tagName === 'code'
) {
const child = node.children[0]
const className = child.properties.className

if (
parent &&
typeof index === 'number' &&
Array.isArray(className) &&
className.some(function (d) {
return d === 'language-mermaid' || d === 'mermaid'
})
) {
parent.children[index] = child
return index
}
}

for (key in urlAttributes) {
if (
Object.hasOwn(urlAttributes, key) &&
Expand Down Expand Up @@ -409,6 +459,91 @@ function post(tree, options) {
}
}

/** @type {Promise<any> | undefined} */
let mermaidPromise

/**
* @param {object} props
* Props.
* @param {string} props.chart
* Chart definition.
* @param {ComponentType<any> | undefined} [props.component]
* Component.
* @param {string} [props.className]
* Class name.
* @returns {ReactElement}
* Element.
*/
/**
* @param {object} props
* Props.
* @param {string} props.chart
* Chart definition.
* @param {ComponentType<any> | undefined} [props.component]
* Component.
* @param {Element} [props.node]
* Node.
* @param {string} [props.className]
* Class name.
* @param {ReadonlyArray<ReactNode> | ReactNode | undefined} [props.children]
* Children.
* @returns {ReactElement}
* Element.
*/
function MermaidGraph({chart, component, node, ...props}) {
const [svg, setSvg] = useState('')
const [error, setError] = useState(false)

useEffect(() => {
let cancelled = false
setError(false)

if (chart) {
if (!mermaidPromise) {
mermaidPromise = import('mermaid').then(({default: mermaid}) => {
mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'strict'
})
return mermaid
})
}

mermaidPromise
.then((mermaid) => {
if (cancelled) return
const id = `mermaid-${Math.random().toString(36).slice(2, 11)}`
return mermaid.render(id, chart.replace(/\n$/, ''))
})
.then((result) => {
if (cancelled || !result) return
setSvg(result.svg)
})
.catch(() => {
if (cancelled) return
setError(true)
})
}

return () => {
cancelled = true
}
}, [chart])

if (error) {
if (component) {
return jsx(component, {node, ...props})
}
return jsx('pre', {children: jsx('code', props)})
}

return jsx('div', {
className: `mermaid ${props.className || ''}`,
dangerouslySetInnerHTML: {__html: svg}
})
}

/**
* Make a URL safe.
*
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"devlop": "^1.0.0",
"hast-util-to-jsx-runtime": "^2.0.0",
"html-url-attributes": "^3.0.0",
"mermaid": "^11.12.2",
"mdast-util-to-hast": "^13.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.0.0",
Expand Down
23 changes: 23 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ React component to render markdown.
(many plugins you can pick and choose from)
* [x] **[compliant][section-syntax]**
(100% to CommonMark, 100% to GFM with a plugin)
* [x] **[mermaid][section-mermaid]**
(built-in support for mermaid diagrams)

## Contents

Expand All @@ -47,6 +49,7 @@ React component to render markdown.
* [Use custom components (syntax highlight)](#use-custom-components-syntax-highlight)
* [Use remark and rehype plugins (math)](#use-remark-and-rehype-plugins-math)
* [Plugins](#plugins)
* [Mermaid](#mermaid)
* [Syntax](#syntax)
* [Compatibility](#compatibility)
* [Architecture](#architecture)
Expand Down Expand Up @@ -595,6 +598,24 @@ Here are three good ways to find plugins:
[`rehype-plugin`][github-topic-rehype-plugin] topics
— any tagged repo on GitHub

## Mermaid

`react-markdown` has built-in support for mermaid diagrams.
You can use code blocks with `mermaid` as the language:

```markdown
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
```

This will automatically render the diagram using [mermaid][mermaid].
The library is lazily loaded, so it won't affect your initial bundle size if you don't use it.

## Syntax

`react-markdown` follows CommonMark, which standardizes the differences between
Expand Down Expand Up @@ -942,3 +963,5 @@ abide by its terms.
[section-syntax]: #syntax

[typescript]: https://www.typescriptlang.org
[mermaid]: https://mermaid-js.github.io/mermaid/
[section-mermaid]: #mermaid
9 changes: 9 additions & 0 deletions test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ test('Markdown', async function (t) {
)
})

await t.test('should support mermaid', function () {
assert.equal(
renderToStaticMarkup(
<Markdown children={'```mermaid\ngraph TD;\n A-->B;\n```'} />
),
'<div class="mermaid language-mermaid"></div>'
)
})

await t.test('should support an html (default)', function () {
assert.equal(
renderToStaticMarkup(<Markdown children="<i>a</i>" />),
Expand Down
Loading