[花果山水帘洞] mistune的KaTeX数学公式扩展开发

介绍了如何开发mistune扩展以支持KaTeX数学公式渲染。

mistune的KaTeX数学公式扩展开发

2017-06-08 / vc12345679

mistune 是一个 单文件的纯 Python markdown 解析器。在读懂 mistune 的源码后,通过继承的方式开发扩展并不太难。

开发前需要了解的 mistune 结构

  1. BlockGrammer()

    1. 含有所有块模式的正则规则 re.compile
  2. BlockLexer()

    1. default_rules 列表:控制块模式匹配顺序
    2. rule = BlockGrammer():块模式匹配规则列表
    3. 对每一个块模式进行匹配的函数,匹配成功后会调用相应的块渲染函数
  3. InlineGrammer()

    1. 含有所有行内模式的正则规则 re.compile
  4. InlineLexer()

    1. default_rules 列表:控制行内模式匹配顺序
    2. rule = InlineGrammer()
    3. 对每一个行内模式进行匹配的函数,匹配成功后会调用相应的行内渲染函数
  5. Renderer()

    1. output_*:针对行内模式的渲染函数
  6. Markdown()

    1. renderer = Renderer()
    2. block = BlockLexer()
    3. inline = InlineLexer()
    4. output_*:针对块模式的渲染函数
    5. parse():解析流程控制,先匹配块模式,再针对块模式无法匹配的内容按顺序进行行内模式匹配

不清楚作者lepture 为什么不把块模式的渲染函数也放到 Renderer() 里,我已在Github上提交了相应的 Issue, 但在得到回复前,我还是按照现有的结构进行开发。

开发思路

块模式扩展开发

  1. 继承 BlockLexer()

    1. 在子类里的 rule 中添加自定义的块模式匹配规则
    2. 向子类中添加自定义的匹配函数
    3. 向子类里的 default_rules 列表中添加自定义的块模式名(注册)
  2. 继承 Markdown()

    1. 添加自定义的 output_* 函数,实现自定义的块模式渲染效果

行内模式扩展开发

  1. 继承 InlineLexer()

    1. 在子类里的 rule 中添加自定义的行内模式匹配规则
    2. 向子类中添加自定义的匹配函数
    3. 向子类里的 default_rules 列表中添加自定义的行内模式名(注册)
  2. 继承 Renderer()

    1. 添加自定义的 output_* 函数,实现自定义的行内渲染效果

一个完整的示例

这个示例来自FMblog, 扩展的目的是为了对自定义的公式内容进行预处理,以便 KaTeX 的js脚本进行渲染。

该示例中包含完整的块模式扩展\\[ ... \\]和行内模式扩展$$ ... $$

需要注意的是,在继承InlineLexer() 时,除添加自定义匹配规则外, 还修改了原本的行内 text 规则,这是因为原本的 text 规则不会在 $ 处断句, 会影响 $$ ... $$ 的正确匹配。

代码

#!/usr/bin/env python3
# encoding=utf-8

__author__ = 'Siwei Chen<me@chensiwei.space>'

import re
from mistune import InlineLexer, Renderer, Markdown, BlockLexer
from mistune_contrib import highlight, toc


class KaTeXRenderer(Renderer, toc.TocMixin, highlight.HighlightMixin):
    def __init__(self, *args, **kwargs):
        super(KaTeXRenderer, self).__init__(*args, **kwargs)

    def inlinekatex(self, text):
        return '<tex class="tex-inline">%s</tex>' % text

    def blockkatex(self, text):
        return '<tex class="tex-block">%s</tex>' % text


class KaTeXInlineLexer(InlineLexer):
    def __init__(self, *args, **kwargs):
        super(KaTeXInlineLexer, self).__init__(*args, **kwargs)
        self.enable_katexinline()

    def enable_katexinline(self):
        self.rules.inlinekatex = re.compile(r'^\${2}([\s\S]*?)\${2}(?!\$)')  # $$tex$$
        self.default_rules.insert(3, 'inlinekatex')
        self.rules.text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~\$]|https?://| {2,}\n|$)')

    def output_inlinekatex(self, m):
        return self.renderer.inlinekatex(m.group(1))


class KaTeXBlockLexer(BlockLexer):
    def __init__(self, *args, **kwargs):
        super(KaTeXBlockLexer, self).__init__(*args, **kwargs)
        self.enable_katexblock()

    def enable_katexblock(self):
        self.rules.blockkatex = re.compile(r'^\\\\\[(.*?)\\\\\]', re.DOTALL)  # \\[ ... \\]
        self.default_rules.insert(0, 'blockkatex')

    def parse_blockkatex(self, m):
        self.tokens.append({
            'type': 'blockkatex',
            'text': m.group(1)
        })


class CustomMarkdown(Markdown):
    def output_blockkatex(self):
        return self.renderer.blockkatex(self.token['text'])

renderer = KaTeXRenderer()
inline = KaTeXInlineLexer(renderer=renderer)
block = KaTeXBlockLexer()
markdown = CustomMarkdown(renderer=renderer, inline=inline, block=block)

功能说明

该扩展会将 \\[ ... \\] 渲染为 <tex class="tex-block"> ... </tex>, 将 $$ ... $$ 渲染为 <tex class="tex-inline"> ... </tex>

配套的 KaTeX js 渲染脚本为

<script type="text/javascript" src="https://cdn.bootcss.com/KaTeX/0.7.1/katex.min.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.bootcss.com/KaTeX/0.7.1/katex.min.css" />
<script type="text/javascript">
    $("tex.tex-inline").each(
        function(i, domElem) {
            katex.render(domElem.innerText, domElem, {displayMode: false});
        });
    $("tex.tex-block").each(
        function(i, domElem) {
            katex.render(domElem.innerText, domElem, {displayMode: true});
        });
</script>