利用DeepSeek将AI输出的混合latex标记的markdown文件渲染成html格式 - l1t1/note GitHub Wiki

在与AI对话过程中,它的输出有很多格式,如果用网页版中的复制按钮,得到的是一个混合latex标记的markdown格式文件。直接将这个格式粘贴到诸如github的编辑框,发表后markdown格式显示了,跨行公式可以显示,行内的不行,显得非常怪异。

我们的需求是将markdown标记转换成html,同时保留latex标记包围的内容,然后用MathJax.js渲染公式。

起初直接问DeepSeek和Qwen3, 两者都给出用marked和MathJax结合的方案。遗憾的是,输出结果中的公式没能转换。而分别用上述两个js工具,无论是单独转latex或markdown标记,都没问题。

改为让DeepSeek自己编程转换,结果总是处理不好带反斜杠的标记,如\(\), 输出遗漏反斜杠,导致渲染失败。

还是回归传统的搜索引擎,找到这篇文章:markdown文档使用mistune转为html,如何保留LaTex中的_不被转码?写mistune子类,介绍用mistune包实现两者同时转换。

我把python代码复制到conv.py, 并把js代码复制到showLaTex.js,执行时报错,说from mistune import Renderer, Markdown, InlineLexer ImportError: cannot import name 'Renderer' from 'mistune' (\Python313\Lib\site-packages\mistune\__init__.py). Did you mean: 'renderers'?。应该是版本问题。

我把conv.py上传到DeepSeek对话,请他基于当前版本 mistune 3 重写,一次成功,完美转换带$的公式,但反斜杠的标记没有处理,跟他说“要不先简单把(和)替换成$,[和]替换成$$,然后再处理?用这个思路写一个函数backslash_to_dollar,插入到html = markdown(text)一行前调用,只输出这个函数”,这次完成得相当好,我测试了包含两种latex标记的文件都转成功了。这样就可以用浏览器回看对话内容,也可以单独把某段对话输出成pdf格式离线查看,而不用庞大的latex工具。

完整的conv.py内容如下

import re
from mistune import HTMLRenderer, create_markdown

class LaTexRenderer(HTMLRenderer):
    def __init__(self, escape=False):  # 禁用自动转义
        super().__init__(escape)
        self.protected = []

    def protect_latex(self, text):
        """保护$$...$$之间的LaTeX内容"""
        def repl(match):
            self.protected.append(match.group(0))
            return f'LATEX_BLOCK_{len(self.protected)-1}_'
        return re.sub(r'\$\$([\s\S]+?)\$\$', repl, text)

    def restore_latex(self, text):
        """恢复被保护的LaTeX块"""
        for i, latex in enumerate(self.protected):
            text = text.replace(f'LATEX_BLOCK_{i}_', latex)
        return text

    def block_text(self, text):
        """处理块级文本时保护LaTeX"""
        protected = self.protect_latex(text)
        result = super().block_text(protected)
        return self.restore_latex(result)

def backslash_to_dollar(text):
    """
    将LaTeX分隔符转换为更通用的格式:
    - 将 \(...\) 转换为 $...$
    - 将 \[...\] 转换为 $$...$$
    同时保留所有反斜杠不变
    
    参数:
        text: 原始Markdown文本
        
    返回:
        转换后的文本,其中LaTeX分隔符已标准化
    """
    # 处理行内公式 \(...\) → $...$
    text = re.sub(
        r'\\\((.*?)\\\)',
        lambda m: f'${m.group(1)}$',
        text,
        flags=re.DOTALL
    )
    
    # 处理块级公式 \[...\] → $$...$$
    text = re.sub(
        r'\\\[(.*?)\\\]',
        lambda m: f'$${m.group(1)}$$',
        text,
        flags=re.DOTALL
    )
    
    return text

def convert_md_to_html(input_path, output_path):
    # 初始化渲染器
    renderer = LaTexRenderer()
    markdown = create_markdown(renderer=renderer)
    
    # 读取Markdown文件
    with open(input_path, 'r', encoding='utf-8') as f:
        text = f.read()
    text=backslash_to_dollar(text)
    # 转换Markdown
    html = markdown(text)
    
    # 添加MathJax支持
    mathjax_js = '''<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script src="showLaTex.js"></script>'''
    
    # 写入输出文件
    with open(output_path, 'w', encoding='utf-8') as f:
        f.write(html + '\n' + mathjax_js)
    
    print(f"Conversion complete. Output length: {len(html)}")

if __name__ == "__main__":
    convert_md_to_html("file.md", "index4.html")

showLaTex.js如下,放在与conv.py同一个目录

/*
* name showLaTex.js
* 依赖于  MathJax.js
* varsion: v0.1
*ES6*/
let isMathjaxConfig = false; // 防止重复调用Config,造成性能损耗
 
const initMathjaxConfig = () => {
  if (!window.MathJax) {
    return;
  }
  window.MathJax.Hub.Config({
    showProcessingMessages: false, //关闭js加载过程信息
    messageStyle: "none", //不显示信息
    jax: ["input/TeX", "output/HTML-CSS"],
    tex2jax: {
      inlineMath: [["$", "$"], ["\\(", "\\)"]], //行内公式选择符
      displayMath: [["$$", "$$"], ["\\[", "\\]"]], //段内公式选择符
      skipTags: ["script", "noscript", "style", "textarea", "pre", "code", "a"] //避开某些标签
    },
    "HTML-CSS": {
      availableFonts: ["STIX", "TeX"], //可选字体
      showMathMenu: false //关闭右击菜单显示
    }
  });
  isMathjaxConfig = true; // 
};
 
 
if (isMathjaxConfig === false) { // 如果:没有配置MathJax
  initMathjaxConfig();
}
 
// 如果,不传入第三个参数,则渲染整个document
window.MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
// 因为使用的Vuejs,所以指明#app,以提高速度
//window.MathJax.Hub.Queue(["Typeset", MathJax.Hub, document.getElementById('app')]);

感兴趣的可以将下述内容保存为file.md,放在与conv.py同一个目录测试。

### 最佳容差(Tolerance, `tol`)计算公式:

对于目标精度为 \( P \) 位十进制数的情况,AGM迭代的容差应设为:

\[
tol = 10^{-P} \cdot \exp\left(-\frac{\pi \cdot P}{2 \ln(10)}\right)
\]

#### 公式推导依据:
1. **AGM收敛性**:每次迭代有效位数约翻倍(二次收敛)
2. **误差传播**:最终误差与初始容差的关系为 \( E_{\text{final}} \approx \frac{\pi}{2} \cdot tol \)
3. **精度匹配**:要求 \( E_{\text{final}} \leq 10^{-P} \)

#### 使用说明:
1. 计算步骤:
   - 先确定目标精度 \( P \)(如100万位则 \( P=10^6 \)- 代入公式计算 \( tol \)
   - 设置GMP上下文精度为 \( \lceil P \cdot \log_2(10) \rceil + 50 \)(二进制位缓冲)

2. 特性:
   -\( P=6\times10^6 \)(600万位)时,\( tol \approx 10^{-6.5\times10^6} \)
   - 自动保证迭代在约 \( \log_2(P) + 4 \) 次后终止

#### 数学验证:
对于43次迭代达到600万位精度的案例:
\[
P=6\times10^6 \implies tol \approx 10^{-6.5\times10^6} \\
\text{迭代次数} = \left\lceil \log_2\left(\frac{\ln(10^{6.5\times10^6})}{\ln(2)}\right) \right\rceil = 43
\]
⚠️ **GitHub.com Fallback** ⚠️