一、导读
我平时写文章都是习惯使用 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-markdown
将 markdown
的内容渲染成 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为#xxx
的HTML
标签对。所以我们想要实现点击TOC
中的标题然后跳转,我们要做两件事:
- 给现有的markdown渲染完成的
HMTL
内容增加id - 给
TOC
中的标题增加herf
跳转功能,herf
中的内容和上面增加的id一一对应
我通过rehype-slug
插件来实现第一点,该插件帮我实现为每一个标题内容生成一个slug id
。
<Markdown className="markdown-body"
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeHighlight, rehypeSlug]}
>
{markdown}
</Markdown>
现在的问题是,我该如何做?后来问了ChatGpt
rehype-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;
}
四、点击?实现跳转
在很多网站上,当鼠标悬停到标题上时,就会出现一个?按钮,点击后就能实现和上述一样的跳转。
如何给标题对应的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
的过程中遇到了很多问题,很感谢网上众多开发者的无私奉献,这里是一些我参考过的文章信息: