起因是本人喜欢在写博客的时候尽量带上目录,可以让整体的结构和思路更加清晰。

但是博客自带的 Markdown 解析库并不是那么好用,虽然能满足基本功能,但是在拓展性上不是很友好。

曾经自己 hook 源代码来实现 TOC 语法,无奈很不优雅,每次升级做 merge 也是很麻烦的事情。

所以干脆整个插件,自己动手,丰衣足食。

自己再去造一个 Markdown 解析器的轮子很显然不合适。于是网上 github 搜索一波,其实 PHP 版本的 Markdown 解析器还真不多。

根据 star 数、活跃度、issue 情况,综合评定下来选择了 erusev/parsedown

关于TOC的支持,目前并没有解决方案。作者 erusev 在这个 issues 上回答了,其并没有打算支持,所以我只能自己来了。

1.0.1

然后有了第一版的 Markdown 解析插件:mrgeneralgoo/typecho-markdown(1.0.1)

这版本主要是干了俩件事:

1.更换 Markdown 解析库,在性能和可拓展性上有较大的提升。

2.新增TOC语法支持。

TOC的支持实现上是通过 DOMDocument来操作解析完后的 DOM 元素,然后添加目录元素到 HTML 里面。这个操作会把 HTML 源码转成 NCR 格式,不方便阅读,但也是 HTML 规范之一,不影响使用。

经过自己使用一个月后很稳定,于是开源出来了。但是其他人使用会出现一些问题,比如在某些场景下会出现乱码

这个可能是DOMDocument处理内容的时候遇到了特殊的字符串处理出现了问题。

虽然采用了处理 DOM 这种通用且暴力的方案,但是后来我发现就 Markdown 生成目录来说,并不需要这种通用的操作方式,只针对 Markdown 处理就好了。

1.1.0

所以这次有了第二版 Markdown 解析插件:mrgeneralgoo/typecho-markdown(1.1.0)

这次主要变更如下:

1.精简结构,移除暂时无用的 erusev/parsedown-extra

2.优化性能,解决乱码问题,使用原生 Markdown 语法来实现目录生成。

3.把TOC解析的逻辑拆分出来,做成了一个单独的 parsedown-extension 库

新版本,性能提升近 20%,并且源码不会再被转成 NCR 格式,乱码问题也被彻底解决。

当然过程中也比较坎坷曲折。

第一就是 erusev/parsedown 很不规范,而且文档也少,大部分情况下只能通过作者自己开发的 erusev/parsedown-extra 来学习,推导如何接入,学习成本很高。

第二是 erusev/parsedown 在代码设计上对拓展并不开放,只适合用来做基类。比如我这里实现了TOC语法,你实现了LaTeX语法,但是我们俩的代码是不能直接一起使用的。得通过 merge 代码的方式,把功能都集成到一个子类当中才行,要是想像一个插件一样安装即用是不可能的。

我也明白,为什么 joyqi 要自己写一个 Markdown 解析器 SegmentFault/HyperDown 了。

Markdown已经面世许多年了,国内外许多大大小小的网站都在用它,但是它的解析器却依然混乱不堪。SegmentFault 是中国较大规模使用 Markdown 语法的网站,我们一直在使用一些开源类库,包括但不限于

  1. php-markdown
  2. CommonMark for PHP
  3. Parsedown

他们都有或多或少的毛病,有的性能较差,有的代码比较业余,更多的情况是由于Markdown本身解析比较复杂,因此我们几乎无法去维护另外一个人写的代码。基于这个原因,我为 SegmentFault 专门编写了这么一个Markdown解析器。

当然,其实有很多设计模式解决第二点,不过鉴于目前还不需要这样做,如果有需要,我还是会尝试一下的😏。

1.1.1

感谢 @fzhihua 反馈的问题,本版本修复多文章列表情况下目录重复问题。

原因在于为了提升性能,解析器使用了单例模式,但是没有做好数据收尾工作,导致了前一篇文章的目录会继续出现在后一篇文章里面,这种 case 只会在多文章情况下会出现,欢迎大家继续反馈 🙇。

1.2.0

本版新增了对 MathJax 语法的支持。

果然,在 1.1.0 版本提到TOCLaTeX语法,这次就有 @yujincheng08 提了 PR 想要支持 MathJax 语法。

这位同学的解决方案没有问题,看得出来还对解析库有一定的了解,所以我测试了下,没问题就给 merge 了。但是从接到 PR 到发布 1.2.0 版本的时候我都在考虑一个问题:如何更方便的拓展解析器的功能。

于是把我对解析库的了解,抽象了 2 个方法来,方便大家理解和接入,这俩个方法分别是:addInlineElementsaddBlockElements 对应的就是相应行元素和块元素的渲染。

然后继续基于元素来分析对应的功能,这次支持 MathJax 语法,关键在于 MathJax 的语法是通过$$$来作为标记符的,标记符包裹的东西会有一些特殊符号,正常情况下会被转义,所以导致公式无法解析成功。

基于此抽象之后本版本要实现的功能其实是:某些标记符包裹的内容不处理

目标清楚之后,对这位同学的 PR 做了一些细微的处理😂,就是这版的更新由来了。

本版本更新的其实是:

1.新增对特定符号包裹的内容不处理 (顺便支持下 MathJax)

2.新增函数使得拓展其功能更加简单 (之所以不叫接口是因为目前的复杂度还没高到要拆分的地步,如果大家踊跃 PR 壮大起来后就可以拆分了)

show 一下 MathJax 的解析效果(需要引入 MathJax 脚本或者使用 MathJax 插件):

$f(x,y,z) = 3y^2z \left( 3+\frac{7x+5}{1+y^2} \right)$

$f(x,y,z) = 3y^2z \left( 3+\frac{7x+5}{1+y^2} \right)​$

$$ F^{HLLC}=\left\{
\begin{array}{rcl}
F_L       &      & {0      <      S_L}\\
F^*_L     &      & {S_L \leq 0 < S_M}\\
F^*_R     &      & {S_M \leq 0 < S_R}\\
F_R       &      & {S_R \leq 0}
\end{array} \right. $$

$$ F^{HLLC}=\left\{
\begin{array}{rcl}
F_L & & {0 < S_L}\\
F^*_L & & {S_L \leq 0 < S_M}\\
F^*_R & & {S_M \leq 0 < S_R}\\
F_R & & {S_R \leq 0}
\end{array} \right. $$

本文由程小白创作,本文可自由转载、引用,但需署名作者且注明文章出处。

原文地址:https://www.chengxiaobai.cn/php/markdown-parser-library.html