react中使用markdown

May 31, 2024

一、导读

我平时写文章都是习惯使用 markdown 格式,最近在创建自己的博客网站的时候,也尝试给自己的博客网站增加 markdown 文章渲染功能。本文将介绍我是如何在 react next.js 项目中实现 markdown 文章的渲染以及TOC的生成和跳转的。

完整的代码请见:tunan.blog github project

二、如何渲染 markdown 文章内容

2.1 使用 react-markdown

在我的项目中我使用的是开源的react-markdown来帮助我来渲染我的文章,如何使用它呢?

第一步,引入 react-markdown

import Markdown from 'react-markdown'

第二步,使用 react-markdown 中的 <Markdown />

<Markdown>
  {content}
</Markdown>

content 是 markdown 原始内容,像下面这样:

# Sample Markdown File

## Introduction
Welcome to the world of Markdown! This file demonstrates the basic syntax elements of Markdown.

## Text Formatting
You can make text **bold** or *italic* easily using Markdown syntax.

## Lists
- Unordered List Item 1
- Unordered List Item 2
  - Sublist Item
- Unordered List Item 3

1. Ordered List Item 1
2. Ordered List Item 2
3. Ordered List Item 3

## Code
Inline code can be highlighted like `this`. For blocks of code, use triple backticks:
```python
def greet():
    print("Hello, Markdown!")
```

react-markdownmarkdown 的内容渲染成 html

<div class="w-3/4"><h1>Sample Markdown File</h1>
<h2>Introduction</h2>
<p>Welcome to the world of Markdown! This file demonstrates the basic syntax elements of Markdown.</p>
<h2>Text Formatting</h2>
<p>You can make text <strong>bold</strong> or <em>italic</em> easily using Markdown syntax.</p>
<h2>Lists</h2>
<ul>
<li>Unordered List Item 1</li>
<li>Unordered List Item 2
<ul>
<li>Sublist Item</li>
</ul>
</li>
<li>Unordered List Item 3</li>
</ul>
<ol>
<li>Ordered List Item 1</li>
<li>Ordered List Item 2</li>
<li>Ordered List Item 3</li>
</ol>
<h2>Code</h2>
<p>Inline code can be highlighted like <code>this</code>. For blocks of code, use triple backticks:</p>
<pre><code class="language-python">def greet():
    print("Hello, Markdown!")
</code></pre></div>

此时这些html的样式可能不是我们想要的,我们可能想要试着修改样式。

2.2 添加样式

我们只要创建一个css文件,并在当前tsx中引入就可以了。可以按照自己的喜爱来创建自己的样式。

import '../../markdown.css'
<Markdown className="markdown-body">
  {markdown}
</Markdown>

我的 markdwon.css:

/** markdown style **/
/** markdown style - text**/
.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 {
    /** use anchor to scroll **/
    scroll-margin: 5rem;
    margin-top: 28px;
    margin-bottom: 16px;
    font-weight: 600;
    line-height: 1.25;
}
.markdown-body h1 {
    font-size: 2em;
    margin-bottom: 16px;
    padding-bottom: .3em;
    border-bottom: 1px solid var(--markdown-divide);
}
……

完整内容请见:markdown.css

2.3 使用插件

这里有很多和react-markdown一起使用的插件,这些插件分为两类:

其中有一些是我目前markdown渲染中所使用到的插件,未来还会根据我的需要继续增加:

使用方式:

<Markdown className="markdown-body"
          remarkPlugins={[remarkGfm]}
          rehypePlugins={[rehypeHighlight, rehypeSlug]}
>
  {markdown}
</Markdown>

三、生成 TOC 展示,并点击跳转

在很多网站上,侧边栏一般都支持文章内容的概览。并且点击这些标题页面就能跳转到对应的内容,这是利用了HTML提供的锚点功能。

3.1 生成 TOC

现在我们实现第一步,如何将文章中的标题信息挑选出来,形成TOC。这里我们主要使用正则表达式。

我们声明这样的一个正则表达式,它表示遇到#且紧跟一个空格符号时就匹配成功,匹配整个字符串,这也是markdown中的标题的编写方式(# 加一个空格,之后增加标题内容):

const regXHeader = /#{1,6}\s.+/g
const titles = markdown.match(regXHeader)

这样就能匹配到所有标题,获得标题的数组,但有时候一些链接和代码块中也存在#符号,所以我们需要去除这些影响:

// remove the special elements
const regexReplaceCode = /(```.+?```)/gms
const regexRemoveLinks = /\[(.*?)\]\(.*?\)/g
const markdownWithoutLinks = content.replace(regexRemoveLinks, "")
const markdownWithoutCodeBlocks =  markdownWithoutLinks.replace(regexReplaceCode, "")

// filter the headers
const regXHeader = /#{1,6}\s.+/g
const titles = markdownWithoutCodeBlocks.match(regXHeader)

现在就可以根据titles的内容就能生成相应的html内容。

titles?.map((tempTitle, i) => {
  const level = (tempTitle?.match(/#/g) || []).length - 1 || 0
  const title = tempTitle.replace(/#/g, "").trim()
  level === 1 ? (globalID += 1) : globalID
  ……
  toc.push({
    level: level,
    id: globalID,
    title: title,
    anchor: anchor,
  })
}
……

3.2 增加锚点实现点击跳转

HTML支持在浏览器url后增加#xxx后就将当前页面跳转到对应的其id为#xxxHTML标签对。所以我们想要实现点击TOC中的标题然后跳转,我们要做两件事:

  • 给现有的markdown渲染完成的HMTL内容增加id
  • TOC中的标题增加herf跳转功能,herf中的内容和上面增加的id一一对应

我通过rehype-slug插件来实现第一点,该插件帮我实现为每一个标题内容生成一个slug id

<Markdown className="markdown-body"
          remarkPlugins={[remarkGfm]}
          rehypePlugins={[rehypeHighlight, rehypeSlug]}
>
  {markdown}
</Markdown>

现在的问题是,我该如何做?后来问了ChatGptrehype-slug是如何实现生成slug id的,它使用了github-slugger算法,于是我也在我的TOC中来使用github-slugger生成slug id. 幸运的是,它奏效了!

import GithubSlugger from 'github-slugger'
const anchor = '#' + slugger.slug(title)

3.3 添加样式

现在回过头来给我们的TOC增加点样式,和给markdown文章内容添加样式一样,只需要声明一个css文件,然后引入到相应的tsx文件即可。

.table-of-contents {
    .level0 {
        font-weight: normal;
        font-size: 17pt;
        font-family: "Microsoft YaHei", serif;
        padding-bottom: 5px;
    }
    .level1 {
        font-weight: normal;
        font-size: 15pt;
        font-family: "Microsoft YaHei", serif;
        padding-bottom: 4px;
    }
    .level2 {
        font-weight: lighter;
        font-size: 13pt;
        font-family: "Microsoft YaHei", serif;
        padding-bottom: 2px;
        padding-left: 10px;
    }
    .level3 {
        font-weight: lighter;
        font-size: 10pt;
        font-family: "Microsoft YaHei", serif;
        padding-bottom: 2px;
        padding-left: 15px;
    }
}

3.4 scroll margin

在我的网站上,我的最上方有一行始终存在悬浮的导航栏,如果直接进行内容跳转,内容会跳转到浏览器页面最上方从而被遮挡,这个时候就需要用到scroll margin样式,从而保证scroll时能够保留上面的部分空间。

所以我的markdown.css文件中会有这个属性的设置。

.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 {
    /** use anchor to scroll **/
    scroll-margin: 5rem;
    margin-top: 28px;
    margin-bottom: 16px;
    font-weight: 600;
    line-height: 1.25;
}

四、点击?实现跳转

在很多网站上,当鼠标悬停到标题上时,就会出现一个?按钮,点击后就能实现和上述一样的跳转。

image-20240604221054565

如何给标题对应的html标签中再增加内容呢?此时就可以用到另一个插件了:rehype-autolink-headings,它支持在标题后面增加内容。使用起来也很简单:

const autolinkHeadingsOptions = {
  behavior: 'append',
  properties: {
    className: ['heading_anchor_svg'],
  },
};

return (
  <Markdown className="markdown-body"
            remarkPlugins={[remarkGfm]}
            rehypePlugins={[rehypeHighlight, rehypeSlug, [rehypeAutolinkHeadings, autolinkHeadingsOptions]]}
  >
    {markdown}
  </Markdown>

)

此时就能在标题后方添加一个class name是heading_anchor_svg的标签对了。此时让我们再在css中生成一下这个链接的SVG图形,markdown.css 中:

/* 当光标悬停在 h 标签上时显示 SVG 图标 */
h1:hover .heading_anchor_svg,
h2:hover .heading_anchor_svg,
h3:hover .heading_anchor_svg,
h4:hover .heading_anchor_svg,
h5:hover .heading_anchor_svg {
    display: inline-block;
    content: var(--markdwon-heading-svg);
    width: 18px;
    margin-left: 10px;
}

其中--markdwon-heading-svg是一个global.css中的全局变量:

--markdwon-heading-svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='18' aria-hidden='true'%3E%3Cpath fill='%23000' d='m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z'%3E%3C/path%3E%3C/svg%3E");

url() 和 var() 不能同时使用,例如:url("data ...var(--xxx)..") 这是不可以的。

到现在,整个markdown文章的渲染就已经全部完成了,想要什么样式可以自己手动去调整,完全可以自己定制。这也是我学习前端的动力,我想自己做出一些内容而不是用现有的框架来搭建我的网站的原因。

但正如我所说的,我是一名后端开发者,前端全都是自己自学的,文章中若有一些不对的地方,请原谅我并请及时联系我从而帮助我改正。谢谢大家。

在学习如何渲染markdown的过程中遇到了很多问题,很感谢网上众多开发者的无私奉献,这里是一些我参考过的文章信息:

  1. MDX.CODE Building a Table of Contents (TOC) from markdown for your React blog
  2. be able to mimic the new heading structure used by github
  3. how to put var into url()
  4. Render Mardown in next.js
Comments