react-markdown:安全渲染Markdown的React组件,支持插件与自定义组件

Markdown component for React

分支4Tags82
当前项目代码仓暂无内容

维护文档说明

  • 定期更新 commonmark-html 链接版本

react-markdown

[![构建][徽章构建图片]][徽章构建链接] [![覆盖率][徽章覆盖率图片]][徽章覆盖率链接] [![下载次数][徽章下载次数图片]][徽章下载次数链接] [![大小][徽章大小图片]][徽章大小链接]

用于渲染 Markdown 的 React 组件。

功能亮点

目录

这是什么?

这个包是一个 [React][] 组件,可以接收一个 markdown 字符串,并将其安全地渲染为 React 元素。 您还可以传递插件来改变 markdown 的转换方式,以及传递将替代普通 HTML 元素的组件。

  • 要学习 markdown,请查看这个 [速查表和教程][commonmark-帮助]
  • 要尝试 react-markdown,请访问 我们的演示

我应该什么时候使用这个?

虽然还有其他在 React 中使用 markdown 的方法,但为什么选择这个?主要有三个原因:它们通常依赖 dangerouslySetInnerHTML,在处理 markdown 时存在 bug,或者不允许您将元素替换为组件。 react-markdown 构建了一个虚拟 DOM,因此 React 只替换发生变化的部分,基于语法树。 这之所以可能,是因为我们使用了 unified,特别是 remark 用于 markdown 和 rehype 用于 HTML, 它们是使用插件转换内容的流行工具。

这个包专注于让初学者能够安全地在 React 中使用 markdown。 当您熟悉 unified 时,您可以使用基于现代钩子的替代方案 react-remarkrehype-react 手动操作。 如果您想在 markdown 文件内使用 JavaScript 和 JSX,请使用 MDX

安装

这个包仅支持 ESM。 在 Node.js(版本 16+)中,使用 [npm][npm-安装] 进行安装:

npm install react-markdown

在 Deno 环境中,使用 esm.sh

import Markdown from 'https://esm.sh/react-markdown@10'

在支持 esm.sh 的浏览器中:

<script type="module">
  import Markdown from 'https://esm.sh/react-markdown@10?bundle'
</script>

用途

一个基本的“你好,世界”示例:

import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'

const markdown = '# Hi, *Pluto*!'

createRoot(document.body).render(<Markdown>{markdown}</Markdown>)
显示等效的 JSX
<h1>
  Hi, <em>Pluto</em>!
</h1>

以下是使用插件的示例 (如remark-gfm,它直接支持脚注、删除线、表格、任务列表和URLs):

import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'

const markdown = `Just a link: www.nasa.gov.`

createRoot(document.body).render(
  <Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>
)
显示等效的 JSX
<p>
  Just a link: <a href="http://www.nasa.gov">www.nasa.gov</a>.
</p>

API

此包导出以下标识符 MarkdownAsyncMarkdownHooks, 以及 defaultUrlTransform。 默认导出为 Markdown

此外,它还导出了额外的 [TypeScript][] 类型 AllowElementComponentsExtraPropsHooksOptionsOptions, 以及 UrlTransform

Markdown

用于渲染 Markdown 的组件。

这是一个同步组件。 使用异步插件时,请参见 MarkdownAsyncMarkdownHooks

参数
返回

React 元素 (ReactElement)。

MarkdownAsync

用于渲染 Markdown 并支持通过 async/await 使用异步插件。

服务器端支持返回 Promise 的组件。 客户端的异步支持,请参见 MarkdownHooks

参数
返回

返回 React 元素的 Promise (Promise<ReactElement>).

MarkdownHooks

通过钩子支持异步插件来渲染 Markdown 的组件。

此组件使用 useEffectuseState 钩子。 钩子运行在客户端,并不会立即渲染内容。 服务器端的异步支持,请参见 MarkdownAsync

参数
返回

React 节点 (ReactNode)。

defaultUrlTransform(url)

使 URL 安全。

这种方式遵循 GitHub 的处理方式。 它允许 httphttpsircircsmailtoxmpp 协议, 以及相对于当前协议的相对 URL(例如 /something)。

参数
  • url (string) —— URL
返回

安全 URL (string)。

AllowElement

过滤元素(TypeScript 类型)。

参数
  • node (Element from hast) —— 需要检查的元素
  • index (number | undefined) —— elementparent 中的索引
  • parent (Node from hast) —— element 的父元素
返回

是否允许 elementboolean,可选)。

Components

将标签名映射到组件(TypeScript 类型)。

类型
import type {ExtraProps} from 'react-markdown'
import type {ComponentProps, ElementType} from 'react'

type Components = {
  [Key in Extract<ElementType, string>]?: ElementType<ComponentProps<Key> & ExtraProps>
}

额外属性

传递给组件的额外字段(TypeScript 类型)。

字段

钩子选项

MarkdownHooks 配置(TypeScript 类型);通过 fallback 属性扩展常规的 Options

扩展

Options

字段

  • fallbackReactNode,可选) —— 在处理器处理 Markdown 时要渲染的内容

选项

配置(TypeScript 类型)。

字段

  • allowElementAllowElementapi-allow-element,可选) —— 过滤元素;首先使用 allowedElements / disallowedElements
  • allowedElementsArray<string>,默认:所有标签名) —— 允许的标签名;不能与 disallowedElements 结合使用
  • childrenstring,可选) —— Markdown
  • componentsComponentsapi-components,可选) —— 将标签名映射到组件
  • disallowedElementsArray<string>,默认:[]) —— 禁止使用的标签名;不能与 allowedElements 结合使用
  • rehypePluginsArray<Plugin>,可选) —— 要使用的 rehype 插件列表
  • remarkPluginsArray<Plugin>,可选) —— 要使用的 remark 插件列表
  • remarkRehypeOptionsremark-rehypeOptions,可选) —— 传递给 remark-rehype 的选项
  • skipHtmlboolean,默认:false) —— 完全忽略 Markdown 中的 HTML
  • unwrapDisallowedboolean,默认:false) —— 提取(展开)禁止元素中的内容; 通常,当说不允许使用 strong 时,它及其子元素会被丢弃,使用 unwrapDisallowed 时,元素本身会被其子元素替换
  • urlTransformUrlTransformapi-url-transform,默认:defaultUrlTransformapi-default-url-transform) —— 改变 URLs

URL转换

转换 URLs(TypeScript 类型)。

参数

  • urlstring) —— URL
  • keystring,例如:'href') —— 属性名
  • nodehast 中的 Element) —— 要检查的元素

返回

转换后的 URL(string,可选)。

示例

使用插件

此示例展示如何使用 remark 插件。 在这个案例中,使用的是 remark-gfm,它直接支持删除线、表格、任务列表和 URLs:

import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'

const markdown = `A paragraph with *emphasis* and **strong importance**.

> A block quote with ~strikethrough~ and a URL: https://reactjs.org.

* Lists
* [ ] todo
* [x] done

A table:

| a | b |
| - | - |
`

createRoot(document.body).render(
  <Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>
)
显示等效 JSX
<>
  <p>
    A paragraph with <em>emphasis</em> and <strong>strong importance</strong>.
  </p>
  <blockquote>
    <p>
      A block quote with <del>strikethrough</del> and a URL:{' '}
      <a href="https://reactjs.org">https://reactjs.org</a>.
    </p>
  </blockquote>
  <ul className="contains-task-list">
    <li>Lists</li>
    <li className="task-list-item">
      <input type="checkbox" disabled /> todo
    </li>
    <li className="task-list-item">
      <input type="checkbox" disabled checked /> done
    </li>
  </ul>
  <p>A table:</p>
  <table>
    <thead>
      <tr>
        <th>a</th>
        <th>b</th>
      </tr>
    </thead>
  </table>
</>

使用带选项的插件

本示例展示了如何使用插件并为其设置选项。要实现这一点,请使用一个数组,插件放在第一位,选项放在第二位。 remark-gfm 提供了一个选项,允许仅使用双波浪线进行删除线的标记:

import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'

const markdown = 'This ~is not~ strikethrough, but ~~this is~~!'

createRoot(document.body).render(
  <Markdown remarkPlugins={[[remarkGfm, {singleTilde: false}]]}>
    {markdown}
  </Markdown>
)
显示等价 JSX
<p>
  This ~is not~ strikethrough, but <del>this is</del>!
</p>

使用自定义组件(语法高亮)

本例展示了如何通过传递一个组件来覆盖元素的默认处理方式。 在此情景中,我们使用了极其出色的 react-syntax-highlighter,由 @conorhastingsgithub-conorhastings 提供,来实现语法高亮:

import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'
import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter'
import {dark} from 'react-syntax-highlighter/dist/esm/styles/prism'

// Did you know you can use tildes instead of backticks for code in markdown? ✨
const markdown = `Here is some JavaScript code:

~~~js
console.log('It works!')
~~~
`

createRoot(document.body).render(
  <Markdown
    children={markdown}
    components={{
      code(props) {
        const {children, className, node, ...rest} = props
        const match = /language-(\w+)/.exec(className || '')
        return match ? (
          <SyntaxHighlighter
            {...rest}
            PreTag="div"
            children={String(children).replace(/\n$/, '')}
            language={match[1]}
            style={dark}
          />
        ) : (
          <code {...rest} className={className}>
            {children}
          </code>
        )
      }
    }}
  />
)
显示等价的 JSX
<>
  <p>Here is some JavaScript code:</p>
  <pre>
    <SyntaxHighlighter language="js" style={dark} PreTag="div" children="console.log('It works!')" />
  </pre>
</>

使用 remark 和 rehype 插件(数学公式)

本示例展示了如何通过语法扩展(通过 remark-math)在 Markdown 中支持数学公式,以及使用转换插件(rehype-katex)来渲染这些数学公式。

import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'
import rehypeKatex from 'rehype-katex'
import remarkMath from 'remark-math'
import 'katex/dist/katex.min.css' // `rehype-katex` does not import the CSS for you

const markdown = `The lift coefficient ($C_L$) is a dimensionless coefficient.`

createRoot(document.body).render(
  <Markdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
    {markdown}
  </Markdown>
)
显示等效的 JSX
<p>
  The lift coefficient (
  <span className="katex">
    <span className="katex-mathml">
      <math xmlns="http://www.w3.org/1998/Math/MathML">{/* … */}</math>
    </span>
    <span className="katex-html" aria-hidden="true">
      {/* … */}
    </span>
  </span>
  ) is a dimensionless coefficient.
</p>

插件

我们使用 unified,特别是 remark 用于 Markdown 和 rehype 用于 HTML, 这些都是通过插件转换内容的工具。以下是有三种寻找插件的好方法:

语法

react-markdown 默认遵循 CommonMark,以标准化不同 Markdown 实现之间的差异。 一些语法扩展通过插件支持。

我们在底层使用 micromark 进行解析。 查阅其文档以获取更多关于 Markdown、CommonMark 和扩展的信息。

兼容性

由 unified 集体维护的项目与维护版本的 Node.js 兼容。

当我们发布一个新的主要版本时,我们会放弃对不再维护的 Node 版本的支持。 这意味着我们尝试保持当前发布线,react-markdown@10,与 Node.js 16 兼容。

它们在所有现代浏览器中都能工作(基本上是除了 IE 11)。 您可以使用打包工具(如 esbuild、webpack 或 Rollup)在您的项目中使用这个包, 并使用其选项(或插件)为旧版浏览器添加支持。

架构

                                                           react-markdown
         +----------------------------------------------------------------------------------------------------------------+
         |                                                                                                                |
         |  +----------+        +----------------+        +---------------+       +----------------+       +------------+ |
         |  |          |        |                |        |               |       |                |       |            | |
markdown-+->+  remark  +-mdast->+ remark 插件 +-mdast->+ remark-rehype +-hast->+ rehype 插件 +-hast->+ 组件 +-+->react 元素
         |  |          |        |                |        |               |       |                |       |            | |
         |  +----------+        +----------------+        +---------------+       +----------------+       +------------+ |
         |                                                                                                                |
         +----------------------------------------------------------------------------------------------------------------+

要了解这个项目的作用,首先重要的是了解 unified 是如何工作的:请阅读 unifiedjs/unified 的自述文件 (直到您看到 API 部分是必读的)。

react-markdown 是一个 unified 管道 —— 包装得如此,大多数用户无需直接与 unified 交互。 处理器会经历以下步骤:

  • 将 Markdown 解析为 mdast(Markdown 语法树)
  • 通过 remark(Markdown 生态系统)进行转换
  • 将 mdast 转换为 hast(HTML 语法树)
  • 通过 rehype(HTML 生态系统)进行转换
  • 使用组件将 hast 渲染为 React

附录 A:Markdown 中的 HTML

react-markdown 通常会转义 HTML(或者在启用 skipHtml 时忽略它),因为它是不安全的,且违背了此库的初衷。

然而,如果您处于受信任的环境(您信任 Markdown),并且可以牺牲包大小(±60kb minzipped),那么您可以使用 rehype-raw

import React from 'react'
import {createRoot} from 'react-dom/client'
import Markdown from 'react-markdown'
import rehypeRaw from 'rehype-raw'

const markdown = `<div class="note">

Some *emphasis* and <strong>strong</strong>!

</div>`

createRoot(document.body).render(
  <Markdown rehypePlugins={[rehypeRaw]}>{markdown}</Markdown>
)
显示等效的 JSX
<div className="note">
  <p>
    Some <em>emphasis</em> and <strong>strong</strong>!
  </p>
</div>

注意:Markdown 中的 HTML 仍然受 CommonMark 中 HTML 的运作方式的限制。确保在包含 Markdown 的块级 HTML 周围使用空行!

附录 B:组件

您还可以更改来自 Markdown 的内容:

<Markdown
  components={{
    // Map `h1` (`# heading`) to use `h2`s.
    h1: 'h2',
    // Rewrite `em`s (`*like so*`) to `i` with a red foreground color.
    em(props) {
      const {node, ...rest} = props
      return <i style={{color: 'red'}} {...rest} />
    }
  }}
/>

组件中的键是您使用 Markdown 编写的元素的 HTML 对应项(例如,h1 对应 # 标题)。 在 Markdown 中,通常包括以下元素:ablockquotebrcodeemh1h2h3h4h5h6hrimgliolpprestrongul。 借助 remark-gfm,您还可以使用 delinputtabletbodytdththeadtr。 其他 remark 或 rehype 插件,如果添加了对新结构支持,同样可以与 react-markdown 配合使用。

传递的属性是您可能期望的:a(链接)将获得 href(以及 title)属性,而 img(图片)将获得 srcalttitle 等。

每个组件都会接收到一个 node。 这是原始的 Element 来自 hast 元素,将被转换成 React 元素。

附录 C:Markdown(和 JSX)中的行结束

您可能会遇到 Markdown 和 JSX 中行结束的处理问题。 我们建议如下,这将解决所有行结束问题:

// If you write actual markdown in your code, put your markdown in a variable;
// **do not indent markdown**:
const markdown = `
# This is perfect!
`

// Pass the value as an expression as an only child:
const result = <Markdown>{markdown}</Markdown>

👆 这有效。

继续阅读了解哪些不行以及原因。

你可能会尝试直接在 JSX 中编写 Markdown 并发现它无法工作:

<Markdown>
  # Hi

  This is **not** a paragraph.
</Markdown>

这是因为在使用 JSX 时,空白字符(包括行结束符)会被折叠为单个空格。 因此,上述示例等价于:

<Markdown> # Hi This is **not** a paragraph. </Markdown>

相反,要将 Markdown 传递给 Markdown,您可以使用一个表达式: 配合模板字符串:

<Markdown>{`
# Hi

This is a paragraph.
`}</Markdown>

模板字面量存在另一个潜在问题,因为它们会保留其中的空白(包括缩进)。 这意味着以下内容不会变成标题:

<Markdown>{`
    # This is **not** a heading, it’s an indented code block
`}</Markdown>

安全性

默认情况下,使用 react-markdown 是安全的。 如果覆盖 urlTransform 为不安全的值,将会使你面临 XSS 攻击向量。 此外,你使用的 remarkPluginsrehypePluginscomponents 可能也不安全。

为了确保内容即使在插件处理后也完全安全,请使用 rehype-sanitize。 它允许你定义自己的允许与禁止的规则。

相关项目

贡献指南

查看 contributing.mdremarkjs/.github 以了解如何开始贡献。 查看 support.md 以获取帮助的方式。

本项目有 行为准则。 通过与这个仓库、组织或社区互动,你同意遵守其条款。

许可

MIT © Espen Hovlandsdal

项目介绍

React 的 Markdown 组件【此简介由AI生成】

定制我的领域
6015.87 K919访问 GitHub