background
Y
K
X
:
S
J
4
%
*
a
A
|
8
a
t
<
;
P
k
Y
s
=
y
-
r
*
u
c
A
O
F
J

favicon
favicon
Reverier 的博客
 

Marked JS 集成 Katex 数学公式渲染

Reverier-Xu at 2023-02-08 11:00:14 Web Development CC-BY-NC-SA 4.0

前言

在内容网站中支持 Markdown 渲染已经是一个很常见的需求了,相比较 Vditormarkdown-it 等重量级 markdown 编辑器与渲染工具来说,用 marked 这类更轻量级的渲染库会带来更好的体验,网站的样式也都可以自己控制。但是 Marked JS 仅支持将基本 Markdown 语法渲染成 HTML 标记,对于 代码块高亮、数学公式还是无能为力的。有关代码高亮官方给出了与 highlightJS 集成的 方式,但是有关集成数学公式渲染的我只搜到了几个 issue 和一些奇怪的实现:

看了后两个现有方案,基本上是用正则表达式给数学公式提取出来,然后塞到 katex 里一顿处理成 html,然后塞回 marked 当成 html 块无脑再渲染一遍。我试了试是能用的,但是行为很奇怪,marked 在处理已经渲染好的 html 块时还会做一些额外的工作,例如转义什么的,最后某些字符总是显示的有问题。

还是看看远处的插件文档,自己写一个插件吧。

Marked JS 插件实现

我打算集成 Katex 而不是 MathJax。因为网站本身不是为了专业的 Markdown 渲染开发的,支持数学公式只是为了让文章阅读更加方便。MathJax 支持很多高级特性,还支持渲染到不同的格式,似乎功能有些冗余,Katex 足够轻量,看起来完全符合我的需求。

Marked 工作机制

在写插件之前,要先了解一下 marked 的工作机制。marked 的渲染流程如下:

在了解这些之后,应该可以发现,只要实现一个能够提取数学公式块的 tokenizer 和一个能够渲染的 renderer,并整合进 marked 的工作流程中,就能够实现数学公式的渲染了。

相关 API

marked 提供了 相关的 API,这里就不当翻译官了。

实现 tokenizer

tokenizer 需要两个,一个用来解决 $f(x)=x+y$ 这样的行内公式,一类用来对付

$$
f(x) = \frac{1}{x}
$$

这类的行间公式。匹配这些我们只需要两个正则表达式就可以了,一个匹配单个 $,一个匹配 $$

实现 render

直接一把梭 katex.renderToString(token.text, options)

代码片段

import katex, { type KatexOptions } from "katex";
import "katex/dist/katex.css";
import type { marked } from "marked";
 
export default function (options: KatexOptions = {}): marked.MarkedExtension {
  return {
    extensions: [inlineKatex(options), blockKatex(options)],
  };
}
 
function inlineKatex(
  options: KatexOptions,
): marked.TokenizerAndRendererExtension {
  return {
    name: "inlineKatex",
    level: "inline",
    start(src: string) {
      return src.indexOf("$");
    },
    tokenizer(src: string, _tokens) {
      const match = src.match(/^\$+([^$\n]+?)\$+/);
      if (match) {
        return {
          type: "inlineKatex",
          raw: match[0],
          text: match[1].trim(),
        };
      }
    },
    renderer(token) {
      return katex.renderToString(token.text, options);
    },
  };
}
 
function blockKatex(
  options: KatexOptions,
): marked.TokenizerAndRendererExtension {
  return {
    name: "blockKatex",
    level: "block",
    start(src: string) {
      return src.indexOf("$$");
    },
    tokenizer(src: string, _tokens) {
      const match = src.match(/^\$\$+\n([^$]+?)\n\$\$/);
      if (match) {
        return {
          type: "blockKatex",
          raw: match[0],
          text: match[1].trim(),
        };
      }
    },
    renderer(token) {
      options.displayMode = true;
      return `<p>${katex.renderToString(token.text, options)}</p>`;
    },
  };
}

保存到 katex_extension.ts 中,使用时只需要导入后 marked.use(KatexExtension({})) 即可,参数中接收的是 Katex 的设置项。

如果需要 lazy load,也可以

const katex = await import("@/path/to/katex_extension.ts");
marked.use(katex.default({ strict: false }));

我先使用不带任何插件的 marked 将基础内容渲染出来,然后再加载 katex 与 highlightJS 重新渲染一遍,在某些网速不佳的环境下能提供更好的用户体验。