php服务端自定义渲染搞不定?试试commonmark
- 作者: 刘杰
- 来源: 技术那些事
- 阅读:100
- 发布: 2025-08-05 07:58
- 最后更新: 2025-08-05 18:32
league/commonmark
这个项目的发现,源于最近要给自己的博客站点进行服务端渲染的升级。最初考虑使用 Parsedown 库进行服务端的解析。不过实际使用后发现 Parsedown 难于扩展,如果拆包即用的话,那确实还不错,运行快,渲染也简单。不过一旦涉及到自定义渲染html结构。就会发现 parseddown 使用起来比较麻烦。
Parsedown优缺点
折腾了一番之后,发现要对 Parsedown 进行扩展,需要继承它的核心类,然后需要重写几个语法解析的关键函数,而且这个极简风格的代码,扩写起来,很容易变成一坨代码,可读性很差。但从单一原则和最少知道原则来讲,这个就完全违背了代码涉及原则。可取之处就是代码的解析逻辑,不过由于没有拆分清晰,读起来也不太容易理解。
CommonMark优点
权衡再三之后,得换一个,这个代码设计的确不适合个人风格。于是找到了 CommonMark 这个项目库。看了下代码结构,这个就舒服多了。
这里说它好,并不是因为谁文件多目录多,谁就有理。从模块化的角度考虑,CommonMark 的模块划分比较清晰。按照职责分成了不同的模块,就是一眼可见的各个文件夹。模块一般是独立存在的,这就给后续的复用提供的可能,同时也将代码的编写范围,划定了一个更加具体的领域。这样一来编码边界就更清晰了,难度也降低了。
自定义html渲染
首先让我们回顾下 Markdown 的语法,从语法结构来看,主要涉及两种,一种是块级元素,另一种就是行级元素。块级元素一般是一整行,或者多行。行级元素语法只能在一行内。
我们要自定义html渲染,主要涉及到这几个模块:
- Node 代码节点,主要涉及 Block、Inline 两种节点
- Parser 代码解析器,主要涉及两种类型的解析器,Block、Inline
- Renderer 渲染器,针对不同的Node类型,有相应的渲染器,需要通过扩展模块,注册Node和Renderer 的关联。
- Extension 扩展功能,主要用来注册Node和渲染器Renderer的映射,解析器映射,事件映射。
简单代码示例
php
// 初始化一个 CommonMarkConverter 对象
$commonMark = new CommonMarkConverter();
// 转换 markdown 源代码并且输出转换后的内容
echo $commonMark->convert("#一级标题测试")->getContent();
以上代码,就可以将一个markdown 源码进行解析了。不过 CommonMark 的核心部分,是一些基础的 markdown 语法。如果涉及到一些复杂一些的语法,就需要额外的扩展模块加持了。比如 Table 扩展,只有加入此扩展才能解析语法中的表格相关的语法。
它这么设计的原则,主要是按需配置,降低复杂语法消耗更多转换时间,在没有需求的情况加就不加。以保持整体逻辑的最简单化。
配置扩展组件
以下代码,将表格组件配置了上去,这样表格语法会自动转换为 html 的 table 结构。
php
// 初始化一个 CommonMarkConverter 对象
$commonMark = new CommonMarkConverter();
// 配置其环境,自定义的配置都在此,可以配置自定义扩展,自定义解析
$commonMark->getEnvironment()
->addExtension(new TableExtension());
// 转换 markdown 源代码并且输出转换后的内容
echo $commonMark->convert("#一级标题测试")->getContent();
看执行结果,未加扩展之前,CommonMark 将表格当做一般的文本处理,只是加了 <p>
标签,加扩展之后,则正常解析成了 table 标签。
配置自定义渲染器
CommonMark 中,实际将 markdown 语法转换为 html 的,是渲染器,不同的语法对应不同的渲染器,每个渲染器有将相应的语法转为对应的 html 的能力。说到这里,如果向扩展自定义的 html 结构,相信大家也能想到了,就是扩展相应的渲染器的功能。
php
// 初始化一个 CommonMarkConverter 对象
$commonMark = new CommonMarkConverter();
$commonMark->getEnvironment()
// 这里是增加了一个自定义的渲染器 FencedCodeRenderer,这个不是默认渲染器
->addRenderer(FencedCode::class, new FencedCodeRenderer());
// 转换 markdown 源代码并且输出转换后的内容
$source = <<<HTML
# 标题
```php
<?php
echo 'hello world';
```
## 二级标题
HTML;
echo $commonMark->convert($source)->getContent();
这里我添加了一个自定的 FencedCodeRenderer
渲染器,注意,虽然名字一样,但是是我自定义的。那么该如何写这个自定义的渲染器呢。很简单,通过将自定义的类继承默认FencedCodeRenderer
渲染器即可,这样可以保证原结构跟现有类一致,而且我们可以通过参照原代码,只更改想要更改的部分就可以。这里我贴一下原 FencedCodeRenderer
代码,渲染方法一目了然,就是 render()
方法。
注意:
FencedCodeRenderer
实际为 final 类,也就是说不让继承。那解决也简单,就是复制一个出来就行。只要保证 namespace 跟原代码不一致,他们就是两个类。添加渲染器的时候用我们自己的类就可以了。
@codeBlock4@
可以随意改写 render
中的html 结构的生成,比如加点自定义的选择器名:
php
return new HtmlElement(
'pre',
['class' => 'myclass', 'id' => '#id-xxx'],
new HtmlElement('code', $attrs->export(), Xml::escape($node->getLiteral()))
);
如果你的需求,仅限于改改现有的一些结构,没有更复杂的,那么到此就可以去试试了。基本能满足你的一些通用的需求了。由于篇幅问题,关于CommonMark更深层的定制开发,后续会逐步完善,请持续关注后续相关文章。