通过对 Python Markdown的拓展来获得类似 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.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的实现使用 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 实现以上拓展并不容易,难点主要在以下两处:
...预排版内容,这是我们期望的。理想的拓展方法是对 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借助 pymdown-extensions 的 tabbed 拓展实现,标记语法如下:
=== "Linux/macOS" ```bash $ pipenv install django ``` === "Windows" ```shell ...\> pipenv install django ```
这段内容将被解析为一段具有 tab 选项卡结构的 HTML 代码段,编写相应的 CSS 样式就可以实现类似 django 文档中那样的命令切换选项卡效果,相关的样式代码可参考博客的源码 frontend/src/style/_tabbed.scss 。
来看看最终的实现效果。
危险
千万不要进行这样的操作:sudo rm -rf /*。
错误
如果这样做,你将造成不可修复的错误。
警告
如果执行了 sudo rm -rf /* 导致系统无法恢复,后果自负。
当心
千万当心在搜索历史命令时不经意间导致 sudo rm -rf /* 命令的执行。
注意
千万注意你的猫在键盘上乱踩时敲出 sudo rm -rf /* 命令。
重要
最好不要在系统中留下 sudo rm -rf /* 的历史记录。
备注
以上内容请切记。
提示
注意 sudo rm -rf /* 后也是可能被恢复的,所以如果你是删库跑路,一定要采取其他措施掩盖你的行径。
小贴士
物理删除不如心理删除。
core/utils.py
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 ''.format(caption, code) {}
Linux/macOS
$ export ENV_VAR=test
Windows
...\> set ENV_VAR=test