前端渲染Markdown技术文档

Fei Yu

渲染展示 markdown 文档是技术博客网站的一个必备功能

一个功能完善且美观的 markdown 文档展示应该支持以下特性:

  • 支持 LaTeX 公式
  • 支持 TableOfContent
  • 支持代码块语法高亮
  • 支持 Mermaid 图

好在轮子都是现成的,下面基于 TypeScript 实现一个 markdownString => htmlString 的渲染函数

interface TocItem {
level: number,
slug: string,
title: string
}

interface RenderResult {
htmlString: string,
tableOfContent: TocItem[]
}

function renderMarkdown(src: string): RenderResult {
// ...
}

Marked.js

marked.js 提供了基础的 markdown 文本解析及 html 生成,并且可以通过对 renderer 部分方法的重写来改变 html 的生成结果以嵌入想要实现的特性

$ yarn add marked @types/marked

渲染函数的基本框架如下

import marked from 'marked'

export const renderMarkdown = (src: string): RenderResult => {
const renderer = new marked.Renderer()
const toc: TocItem[] = []

// override renderer methods

html = marked(src, {renderer: renderer})

return {htmlString: html, tableOfContent: toc}
}

LaTeX

技术文档中有时候需要使用基于 语法的数学公式和符号,如含时薛定谔方程:

使用 KaTeX 进行 语法的解析与公式生成

$ yarn add katex @types/katex
import {renderToString} from 'katex'
import 'katex/dist/katex.css'

一般常用的公式有两种形式:

  1. 行间公式:由 $$...$$ 包裹的单独段落
  2. 行内公式:由 $...$ 包裹的文本

重写 renderer.paragraph 方法,判断是否是行间公式或包含行内公式,并进行内容的替换

renderer.paragraph = function (text: string): string {

const isTeXInline: boolean = /\$(.*)\$/g.test(text)
const isTeXLine: boolean = /^\$\$(\s*.*\s*)\$\$$/.test(text)

if (isTeXLine) {
text = "<div class=\"marked-tex\">" +
renderToString(text.replace(/\$/g, ""))
+ "</div>"
}

else if (isTeXInline) {
text = text.replace(
/(\$([^$]*)\$)+/g,
function (match: string, p: string): string {
return "<span class=\"marked-inline-tex\">" +
renderToString(p.replace(/\$/g, ""))
+ "</span>"
})
}

return '<p>' + text + '</p>\n';
}

Table of Content

部分 markdown 编辑器(如 Typora)支持使用 [TOC] 语法插入目录,但对于网站来说目录更适合单独放在页面中其它位置,因此通过渲染函数返回ToC内容,而不是将目录嵌入在文档中

对于有固定顶部导航的页面,需要通过 scroll-margin-top 保证使用 hash tag 定位跳转时顶部导航不会遮挡跳转的位置

renderer.heading = function (text: string, level: 1|2|3|4|5|6, raw: string, slugger: Slugger): string {

const slug = this.options.headerPrefix + slugger.slug(raw)

toc.push({
slug: slug,
level: level,
title: text
})

return '<h'
+ level
+ ' id="'
+ slug
+ '" style="scroll-margin-top: var(--scroll-offset);">'
+ text
+ '</h'
+ level
+ '>\n';
}

注意在marked函数中为slug添加前缀,避免和页面其它标签id冲突

const html = marked(src, {
renderer: renderer,
headerPrefix: "h-"
})

Code Block

使用 highlight.js 来进行代码块的语法高亮处理

Mermaid 是一个通过纯文本画各种图表的JS库,比如一个OAuth应用的接口请求时序图:

sequenceDiagram
  participant Client
  participant Authorization Server
  participant API
  Client->>Authorization Server: Perform OAuth 2.0 Flow
  Authorization Server->>Client: Access Token
  Client->>API: Request with Access Token
  API->Authorization Server: Validates Access Token
  API->>Client: Response

由于绘图需要使用类型为 mermaid 的代码块,因此在对代码块的高亮处理中嵌入对 mermaid 图的解析与渲染

$ yarn add highlight.js
$ yarn add mermaid @types/mermaid

highlight.js/styles/ 中有许多样式可供选择

import mermaid from 'mermaid'
import highlight from "highlight.js"
import 'highlight.js/styles/github-gist.css'

在 marked 函数中传入 highlight 函数,并单独处理 mermaid 类型

const html = marked(src, {
renderer: renderer,
highlight: function (code: string, language: string): string {

if (language == "mermaid") {
return mermaid.render("mermaid", code)
}

const validLanguage = highlight.getLanguage(language) ? language : 'plaintext'
return highlight.highlight(code, {language: validLanguage}).value
},
headerPrefix: "h-"
})
此页目录
前端渲染Markdown技术文档