python粘性拓展_拓展Python Markdown
通过拓展 Python Markdown 来获得类似 django 官方文档的阅读体验。
最近阅读 django 的官方文档,发现一些很细节的文档内容展现形式,能够极大地提高文档的阅读体验。阅读其他技术文档时也会经常发现类似的内容展现形式。我的博客主要也是发布一些技术类文章,于是决定实现类似的功能以增强读者阅读博客文章的体验。
确定需求后,简单地研究了一下实现方式,然后花了一个晚上的时间把功能上线了,在这里分享记录一下整个功能的实现过程。
确定需求
阅读技术类文档经常会看到这么几种内容:Code block、Admonition、Command tab。中文不太好翻译,来看一下实际的效果就知道了,下面是 django 中这几种内容的展现形式。
Code block
代码块的上方有一个 header,左边显示代码块所在文件路径,这样示例代码应该放在哪个文件就一目了然;右边是一个按钮,点击即可复制整个代码块中的内容。
Admonition
admonition 用来展现一些提示、警告等内容,文档中经常见到的有危险(danger)、警告(warning)、注意(attention)、重要(important)、提示(hint)等内容,不同类型的内容通常会以不同的背景和字体颜色区分。
Command tab
技术类文档中少不了系统命令,很多相同效果的命令在不同操作系统中的字符内容是有一定差异的。写的不太好的文档通常只给出 Linux 下的执行命令;好点的文档则将执行命令分别列出;而 django 文档的处理就非常细节,以 tab 切换的形式给出不同系统下的命令执行方式,这样既能够列出不同系统下的执行命令,又不会重复占用文档的内容空间,提高了文档的紧凑感和阅读时的流畅性。
我的需求就是要在自己博客文章中实现以上三种内容展现效果。
方案研究
博客文章的标记语言采用的是 Markdown,具体的实现采用的是 Python-Markdown/markdown 这个开源库。这个库不仅实现了 Markdown 标准语法的解析,还提供了很多丰富的拓展语法。
例如需求中提到的 admonition 功能,通过添加 markdown.extensions.admonition 拓展就可以直接实现(具体的实现原理和使用方式下面会介绍)。
Code block 的功能也有相应的拓展来实现的,但是调研发现官方自带拓展的功能弱了一点,无法通过拓展的语法在代码块的上方添加 header,只能部分满足需求。开源的第三方拓展中也没有找到可满足需求的实现,所以这里可能需要自己拓展实现。
Command tab 功能的实现在 markdown 的第三方拓展库 facelessuser/pymdown-extensions 中找到了一个 tabbed 拓展,提供的标记语法可被解析生成一个 tab 选项卡,完美满足需求。
至此,实现方案基本就可以确定了:
admonition 功能,直接使用 markdown 库的官方 admonition 拓展就可以;
Code block 在 pymdown-extensions 中有一个更好的拓展实现,叫做 SuperFences,但是还是无法满足生成代码块 header 的需求,因此我们考虑对 SuperFences 再做进一步拓展;
Command tab 使用 pymdown-extensions 的 tabbed 拓展可完美满足需求。
具体实现
Admonition
admonition 的实现最为简单,只需引入官方 markdown.extensions.admonition 拓展就可以了。它的实现原理是通过下面的语法标记 admonition 的内容:
!!! note "注意"
请注意这段内容!
markdown 会把标记内容解析为下面的 HTML 文本:
注意
请注意这段内容!
编写适当的 CSS 样式,就可以达到类似 django 文档中那样的展示效果了。
参考资料
markdown.extensions.admonition 拓展的使用可参考官方文档 Admonition。
拓展的引入方式可参考博客项目的源码 blogproject/core/utils.py#L57。
admonition 的 CSS 样式可参考博客中的源码 frontend/src/style/_admonition.scss。
Code Block
code block 的实现使用 pymdown-extensions 中 SuperFences 拓展,不过遗憾的是,SuperFences 没有在代码块头部添加 header 内容的功能,这样就无法展示代码块所在的文件路径等信息了。花了不少时间读了一下 SuperFences 的源码,遗憾地发现 SuperFences 并没有暴露什么便捷的接口用于对已解析后的内容做进一步加工,如果通过继承等方式进行拓展的话可能需要覆盖重写大量方法,最后决定用一种 monkey patch 的方式进行拓展,以便使需要改动的代码量最小。
首先来看看 SuperFences 提供的代码块标记语法:
```python linenums="1"def print_hello_world():
print("hello world")
```
注意到高亮的第一行代码,python 指定代码块中代码属于何种编程语言,其后紧跟的 key=value 形式的键值对是拓展选项(linenums 是代码行号拓展,指定后解析的代码块中的代码将包含代码行号)。
解析后的 HTML 文档大致如下:
...
可惜 SuperFences 原生只提供 linenums、hl_lines 两个拓展选项,我们希望能够添加一个拓展选项 filename,用于指定代码块所属文件路径,并将其值添加到解析后的代码块头部。标记语法如下:
```python linenums="1" filename="pyproject/hello_world.py"def print_hello_world():
print("hello world")
```
预期的解析效果:
...
不过想基于 SuperFences 实现以上拓展并不容易,难点主要在以下两处:
SuperFences 在解析内容时会校验拓展选项,默认的校验器(validator)只接受 linenums、hl_lines 两个拓展选项,任何多余的选项都无法通过校验,所以我们添加的 filename 拓展选项就无法通过校验,而 SuperFences 并未暴露任何接口可以替换掉默认的校验器。
SuperFences 最终会调用 SuperFencesBlockPreprocessor.highlight 实例方法对代码块做代码高亮处理,然后返回
...预排版内容,这是我们期望的。理想的拓展方法是对 highlight 方法返回的内容再进行包装,即在外层再包上 filename 选项的内容,但是 SuperFences 并未暴露任何接口可以替换 SuperFencesBlockPreprocessor 类,这样就无法通过继承覆盖重写 highlight 方法的方式增强 SuperFencesBlockPreprocessor。
好在 Python 语言足够灵活,我们可以通过 monkey patch 的方式以最小代码 kill 掉上述两个难点。
对于难点 1,SuperFences 使用的默认校验器 highlight_validator 是定义在 pymdownx.superfences 模块中的顶层函数,因此这里采用的方式就是在 SuperFences 调用这个函数之前,将 highlight_validator 替换为我们自定义的函数,这在 Python 中实现非常简单:
import pymdownx.superfences
pymdownx.superfences.highlight_validator = _highlight_validator
_highlight_validator 是我们自定义的函数,放宽了原校验函数的校验逻辑,具体的实现代码可参考本博客的源码 blogproject/core/utils.py#L18。
对于难点 2,想要对一个类方法返回的结果进一步包装,自然想到类方法装饰器。首先实现一个装饰器,对 highlight 方法返回的结果进行进一步的处理,然后再用 monkey patch 的方式将 SuperFencesBlockPreprocessor.highlight 方法替换为装饰后的方法。具体的实现代码请参考博客的源码 blogproject/core/utils.py#L26。
最后编写适当的 CSS 样式,就可以达到类似 django 文档中代码块那样的展示效果了。相关的样式代码可参考博客的源码 frontend/src/style/_literal.scss。
参考资料
SuperFences 拓展还提供了很多丰富的功能,具体使用方式可参考其官方文档 SuperFences。
Command Tab
Command tab 借助 pymdown-extensions 的 tabbed 拓展实现,标记语法如下:
=== "Linux/macOS"
```bash
$ pipenv install django
```
=== "Windows"
```shell
...\> pipenv install django
```
这段内容将被解析为一段具有 tab 选项卡结构的 HTML 代码段,编写相应的 CSS 样式就可以实现类似 django 文档中那样的命令切换选项卡效果,相关的样式代码可参考博客的源码 frontend/src/style/_tabbed.scss。
效果演示
来看看最终的实现效果。
Admonition
危险
千万不要进行这样的操作:sudo rm -rf /*。
错误
如果这样做,你将造成不可修复的错误。
警告
如果执行了 sudo rm -rf /* 导致系统无法恢复,后果自负。
当心
千万当心在搜索历史命令时不经意间导致 sudo rm -rf /* 命令的执行。
注意
千万注意你的猫在键盘上乱踩时敲出 sudo rm -rf /* 命令。
重要
最好不要在系统中留下 sudo rm -rf /* 的历史记录。
备注
以上内容请切记。
提示
注意 sudo rm -rf /* 后也是可能被恢复的,所以如果你是删库跑路,一定要采取其他措施掩盖你的行径。
小贴士
物理删除不如心理删除。
Code Block
python filename="core/utils.py" linenums="1"
def caption_fence_code_format(source, language, css_class, options, md):
code = fence_code_format(source, language, css_class, options, md)
caption = options.get("filename", "")
if caption == "":
return code
return '
Command Tab
Linux/macOS
bash
$ export ENV_VAR=test
Windows
shell
...\> set ENV_VAR=test
致谢
感谢老婆大人在前端方面给予的指点。
-- EOF --
微信
支付宝
9.9
19.9
29.9
59.9
任意
赞赏