soarli

近乎完美地解决MathJax与Marked的冲突
本文由AI辅助撰写,可能存在不准确之处,请读者注意甄别!在文章《让MathJax更好地兼容谷歌翻译和延时加载》中,...
扫描右侧二维码阅读全文
19
2024/09

近乎完美地解决MathJax与Marked的冲突

本文由AI辅助撰写,可能存在不准确之处,请读者注意甄别!

在文章《让MathJax更好地兼容谷歌翻译和延时加载》中,原作者探讨了Cool Papers引入MathJax解析LaTeX公式后的兼容性问题。虽已解决部分问题,但在Cool Papers中,MathJax与Markdown渲染库Marked之间的冲突依然存在。这篇文章将专注于如何完美解决这个问题。

问题背景

Markdown是一种轻量级的标记语言,广泛用于编写文档。Cool Papers使用的Kimi功能输出的是Markdown语法,并通过Marked库在前端将其转换为HTML。然而,LaTeX公式与Markdown有部分语法重合,这导致Marked可能会错误处理LaTeX代码,导致MathJax无法正确渲染公式。

一个简单的代码展示了问题所在:

<div id="content"></div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@2.7.9/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script>
    var div = document.getElementById('content');
    div.innerHTML = marked.parse('**cannot** render: \\(a^2 + b^2\\), **can** render: \\\\(c^2 + d^2\\\\)');
    MathJax.Hub.Typeset(div);
</script>

在这段代码中,\\(a^2 + b^2\\)\\\\(c^2 + d^2\\\\)代表LaTeX公式,但只有后者能被MathJax正确渲染。原因是Marked误解析了部分LaTeX代码,导致公式失效。

已有解决方案

目前,社区已经提出了几种解决MathJax与Marked冲突的方法:

  1. 手动转义:通过手动修改LaTeX代码以适应Marked的解析方式,虽然有效但不直观,且Cool Papers使用的公式是自动生成的,不能轻易修改。
  2. 保护公式:将LaTeX代码放在代码块中避免被Marked解析,但这种方式容易与实际的代码块混淆。
  3. 更换引擎:使用更好的渲染引擎,如Pandoc,但这种方案主要针对后端,前端渲染没有很好的替代方案。
  4. 修改Marked引擎:修改Marked的规则,避免其解析LaTeX代码。但这种方式治标不治本,需要不断手动调整规则。

这些方案都存在一定的局限性,因此我们需要一个更加优雅的解决方案。

逆向思路:先MathJax,后Marked

问题的根源在于Marked与MathJax的渲染顺序——Marked会误解析LaTeX代码。那么,如果我们反过来处理,先用MathJax渲染公式,再用Marked处理Markdown内容,这样便可以从根本上解决问题。

以下是实现该思路的代码:

<div id="content"></div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@2.7.9/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script>
    var div = document.getElementById('content');
    div.innerHTML = '**can** render: \\(a^2 + b^2\\)';
    MathJax.Hub.Queue(
        ['Typeset', MathJax.Hub, div],
        function() {
            div.innerHTML = marked.parse(div.innerHTML);
        }
    );
</script>

该方案能够让MathJax优先处理LaTeX公式,确保公式能够被正确解析,然后再由Marked处理剩余的Markdown内容。

进一步优化

尽管上述方案能够有效解决问题,但对于有“强迫症”的用户来说,仍有两个小瑕疵:

1. 初始页面显示乱码

在渲染完成之前,用户会短暂看到未处理的Markdown文本,类似乱码。为了改善这一体验,可以在渲染完成后再将内容显示给用户:

<div id="content"></div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@2.7.9/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script>
    var div = document.getElementById('content');
    var div2 = document.createElement('div');
    div2.innerHTML = '**can** render: \\(a^2 + b^2\\)';
    MathJax.Hub.Queue(
        ['Typeset', MathJax.Hub, div2],
        function() {
            div.innerHTML = marked.parse(div2.innerHTML);
        }
    );
</script>

通过在隐藏的元素中进行渲染,用户可以直接看到最终渲染结果,避免看到中间的乱码。

2. MathJax菜单消失

另一个问题是,MathJax自带的右键菜单功能消失了。原因是,重新渲染HTML内容时,绑定在公式上的事件监听器被移除了。为了解决这个问题,可以在Markdown解析后,重新调用MathJax的Typeset方法来渲染公式:

<div id="content"></div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@2.7.9/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script>
    var div = document.getElementById('content');
    var div2 = document.createElement('div');
    div2.innerHTML = '**can** render: \\(a^2 + b^2\\)';
    MathJax.Hub.Queue(
        ['Typeset', MathJax.Hub, div2],
        function() {
            div.innerHTML = marked.parse(div2.innerHTML);
            div.querySelectorAll('.MathJax').forEach(e => e.remove());
            MathJax.Hub.Typeset(div);
        }
    );
</script>

在此代码中,删除已渲染的公式并重新调用Typeset确保MathJax功能正常。

3. 保存原始公式代码

Marked会错误解析嵌入的<script>标签,导致MathJax的渲染再次出错。为此,我们可以在marked.parse之前保存公式代码,渲染后再覆盖回去:

<div id="content"></div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@2.7.9/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script>
    function parseMarkdown(text) {
        var scripts = text.match(/<script[^>]*>([\s\S]*?)<\/script>/gi);
        text = marked.parse(text);
        return text.replace(/<script[^>]*>([\s\S]*?)<\/script>/gi, m => scripts.shift());
    }
    var div = document.getElementById('content');
    var div2 = document.createElement('div');
    div2.innerHTML = '**can** render: \\(J\'_\\theta = J_\\theta\\)';
    MathJax.Hub.Queue(
        ['Typeset', MathJax.Hub, div2],
        function() {
            div.innerHTML = parseMarkdown(div2.innerHTML);
            div.querySelectorAll('.MathJax').forEach(e => e.remove());
            MathJax.Hub.Typeset(div);
        }
    );
</script>

通过这种方式,我们能够避免Marked对公式进行错误解析,同时保证MathJax的正常渲染。

结论

通过逆向思路,先用MathJax处理公式,再用Marked渲染Markdown,我们可以有效解决MathJax与Marked的冲突问题,并进一步通过优化,提升页面的显示效果和用户体验。这种方法不仅适用于前端渲染的场景,也可以为后端渲染提供参考。

参考资料:

近乎完美地解决MathJax与Marked的冲突 - 科学空间|Scientific Spaces

给自己的站点增加基于MathJax的LaTeX公式支持 - 知乎

最后修改:2024 年 09 月 19 日 10 : 04 PM

发表评论