让MathJax更好地兼容谷歌翻译和延时加载
By 苏剑林 | 2024-08-15 | 15516位读者 |很早之前,就有读者提出希望把Cool Papers上面的数学公式渲染一下,因为很多偏数学的论文,它们的摘要甚至标题上都带有LaTeX代码写的数学公式,如果不把这些公式渲染出来,那么看上去就像是一堆乱码,确实会比较影响阅读体验。然而,之前的测试显示,负责渲染公式的MathJax跟谷歌翻译和延时加载都不大兼容,所以尽管需求存在已久,但笔者一直没有把它加上去。
不过好消息是,经过反复查阅和调试,这两天笔者总算把兼容性问题解决了,所以现在大家看到的Cool Papers已经能够渲染数学公式了。这篇文章总结一下解决方案,供大家参考。
公式渲染 #
对于在网页中显示数学公式(LaTeX),目前有两个比较主流的方案,分别是MathJax和KaTeX。KaTeX相对来说更为轻量级一些,但它对LaTeX的支持不如MathJax全面,加之本博客一直都是用MathJax,所以在考虑给Cool Papers加数学公式支持时,也是第一时间选择MathJax。
跟Python类似,MathJax的3.x跟2.x是两套差别比较大的体系(最新版本是3.2.2,4.0版也已经开始测试了),而目前我们能搜到的多数MathJax相关资料都是2.x版的,所以给Cool Papers上的是MathJax 2.x的最新版2.7.9(也是本博客用的版本,特别地,arXiv官网也用MathJax,用的是2.7.3版)。
对于一个普通的网页,给它加上数学公式渲染功能并不难,在网页中加两段代码即可,以下是本博客加的参考代码:
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']]},
TeX: {equationNumbers: {autoNumber: ["AMS"], useLabelIds: true}, extensions: ["AMSmath.js", "AMSsymbols.js", "extpfeil.js"]},
"HTML-CSS": {linebreaks: {automatic: true, width: "95% container"}, noReflows: false, availableFonts: ["tex"], styles: {".MathJax_Display": {margin: "1em 0em 0.7em;", display: "inline-block!important;"}}},
"CommonHTML": {linebreaks: {automatic: true, width: "95% container"}, noReflows: false, availableFonts: ["tex"], styles: {".MJXc-display": {margin: "1em 0em 0.7em;", display: "inline-block!important;"}}},
"SVG": {linebreaks: {automatic: true, width: "95% container"}, styles: {".MathJax_SVG_Display": {margin: "1em 0em 0.7em;", display: "inline-block!important;"}}},
"PreviewHTML": {linebreaks: {automatic: true, width: "95% container"}}
});
</script>
<script src="/static/MathJax-2.7.9/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
翻译之殇 #
以上代码对于普通需求来说是有效的,可以成功将LaTeX代码转为网页可显示的数学公式。但对于Cool Papers来说,它遇到了两个“拦路虎”:网页翻译和延时加载。这一节我们首先来解决第一个“拦路虎”——网页翻译,这里主要指Chrome浏览器自带的谷歌翻译。
众所周知,Cool Papers的主旨是刷论文,论文的标题和摘要都是英文的,而对于母语是中文的我们来说,很多时候都会打开网页翻译功能来加速阅读。虽然肯定也有读者对此“不屑一顾”,认为直接阅读原始英文描述更精准,但不可否认网页翻译的需求确实存在,而且对于“刷论文”这个目标来说,很多时候即便只看机翻为中文的内容也够用了。
然而,对于带有经MathJax渲染的数学公式的页面,谷歌翻译后的效果可谓“面目全非”,近乎乱码到没法阅读下去。读者可以自行到arXiv找一篇带有公式的论文尝试一下,比如2408.07010的结果如下:
免翻金牌 #
解决这个问题的思路就是给公式加个“免翻金牌”,即不要翻译公式。经过搜索,发现让谷歌翻译不翻译某个元素的方法有两个,一是给该元素增加一个类名class="notranslate"
,二是给该元素增加一个属性translate="no"
。增加的方式又有两种,一种是在后端增加,即在服务端输出之前就修改好网页内容,另一种是在前端增加,即浏览器接收完网页内容后再用JS修改。
对于MathJax来说,它是在前端实时渲染的数学公式,后端接触不到渲染后的公式,所以我们只能选择在前端增加的方案。经过测试发现,MathJax会给渲染后的数学公式加上MathJax
的class名,所以我们根据该class名称就可以抽取到所有公式,然后就可以通过JS追加class="notranslate"
,参考代码如下:
document.querySelectorAll('.MathJax').forEach(element => element.classList.add('notranslate'));
但是要注意,这句代码需要在所有数学公式都渲染完毕后运行才有效。如何保证公式都已经渲染完呢?最可靠的方案,是将该代码放置到MathJax的Queue中去(参考这里):
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']]},
TeX: {equationNumbers: {autoNumber: ["AMS"], useLabelIds: true}, extensions: ["AMSmath.js", "AMSsymbols.js", "extpfeil.js"]},
"HTML-CSS": {linebreaks: {automatic: true, width: "95% container"}, noReflows: false, availableFonts: ["tex"], styles: {".MathJax_Display": {margin: "1em 0em 0.7em;", display: "inline-block!important;"}}},
"CommonHTML": {linebreaks: {automatic: true, width: "95% container"}, noReflows: false, availableFonts: ["tex"], styles: {".MJXc-display": {margin: "1em 0em 0.7em;", display: "inline-block!important;"}}},
"SVG": {linebreaks: {automatic: true, width: "95% container"}, styles: {".MathJax_SVG_Display": {margin: "1em 0em 0.7em;", display: "inline-block!important;"}}},
"PreviewHTML": {linebreaks: {automatic: true, width: "95% container"}}
});
MathJax.Hub.Queue(function() {
document.querySelectorAll('.MathJax').forEach(element => element.classList.add('notranslate'));
});
</script>
<script src="/static/MathJax-2.7.9/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
这样的意思就是让MathJax自己在确保公式渲染完后,再去执行MathJax.Hub.Queue
里边定义的函数。经过这样处理,在公式渲染完毕后再打开谷歌翻译,公式就不会被翻译了。
延时加载 #
MathJax的第二个“拦路虎”是延时加载。在Cool Papers的列表页中,由于要显示的论文数可能比较多(数百乃至上千),所以刚开始打开网页的时候,不会一次性加载所有论文,而且只显示前25篇,直到滑动到接近底部了,才继续加载下一个25篇,这样提高了效应速度,同时也几乎不会影响用户体验,这就是延时加载。
很多内容比较多的网站都会采用延时加载,这是一个比较成熟的技术了。但对于MathJax来说,它只会渲染刚打开页面时的所有公式,后面延时加载的内容里边的公式则不会主动渲染,所以要在延时加载之后手动渲染一下。这个倒是不难,手动触发渲染的函数是MathJax.Hub.Typeset
,我们只需要在延时加载的函数后面加上这个函数就行,类似于:
loadMorePapers();
MathJax.Hub.Typeset();
注意单纯的MathJax.Hub.Typeset
不包含给公式追加notranslate
的操作,要补上这个操作,则需要改为:
loadMorePapers();
MathJax.Hub.Queue(
['Typeset', MathJax.Hub],
function() {
document.querySelectorAll('.MathJax').forEach(element => element.classList.add('notranslate'));
}
);
混合双殇 #
上面我们分别解决了MathJax与谷歌翻译、延时加载的兼容性问题,然而,当谷歌翻译跟延时翻译一起出现时,新的问题出现了。
假设我们刚进入页面时就打开了谷歌翻译,那么当我们浏览到接近底部时,新的论文被延时加载近来,然后谷歌翻译会被继续触发,进而把新加载的论文也翻译了。如果我们还设置了上一节的手动渲染公式代码,那么公式也会被MathJax渲染出来。由于谷歌翻译和MathJax同时触发,但公式渲染完才会给公式追加class="notranslate"
,换句话说翻译开始进行时公式都还没来得及加上class="notranslate"
,所以最终公式还是被翻译了。
要解决这个问题,就必须想一个办法,确保在公式渲染完毕且加上class="notranslate"
后才执行谷歌翻译。可是,谷歌翻译是Chrome自带的,我们没法通过网站来操纵浏览器的行为。看上去进入了死胡同,不过笔者测试发现,谷歌翻译会随时监控页面的变化,来决定是否触发新的翻译。根据这个特性,我们可以来个“逆向思维”。
怎么个逆向法呢?我们知道,对于Cool Papers来说,要翻译的无非就是论文的标题和摘要,我们可以在一开始就它们加上class="notranslate"
,加上后不管是首次浏览还是延时加载,谷歌翻译都不会主动翻译它们。然后,我们可以在公式渲染完成之后,再把标题和摘要的class="notranslate"
去掉,此时浏览器就会识别到标题和摘要是可翻译的内容了,然后翻译就会被触发。
这样一来,我们就成功实现了确保公式渲染完成后才触发翻译功能了。参考代码如下:
loadMorePapers();
MathJax.Hub.Queue(
['Typeset', MathJax.Hub],
function() {
document.querySelectorAll('.MathJax').forEach(element => element.classList.add('notranslate'));
document.querySelectorAll('a.title-link, p.summary').forEach(element => element.classList.remove('notranslate'));
}
);
总结一下 #
最后,我们来总结一下我们的解决方案。如果你的网站需要显示数学公式,带有延时加载功能,并且用户有网页翻译功能的需求,那么可以按照如下步骤,达到最大的兼容性:
1、给所有带有公式的内容块,都加上class="notranslate"
;
2、按照如下方式,加载MathJax(其中“a.title-link”、“p.summary”是带有数学公式的块的class名之一):
<script type="text/x-mathjax-config">
MathJax.Hub.Config({
tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']]},
TeX: {equationNumbers: {autoNumber: ["AMS"], useLabelIds: true}, extensions: ["AMSmath.js", "AMSsymbols.js", "extpfeil.js"]},
"HTML-CSS": {linebreaks: {automatic: true, width: "95% container"}, noReflows: false, availableFonts: ["tex"], styles: {".MathJax_Display": {margin: "1em 0em 0.7em;", display: "inline-block!important;"}}},
"CommonHTML": {linebreaks: {automatic: true, width: "95% container"}, noReflows: false, availableFonts: ["tex"], styles: {".MJXc-display": {margin: "1em 0em 0.7em;", display: "inline-block!important;"}}},
"SVG": {linebreaks: {automatic: true, width: "95% container"}, styles: {".MathJax_SVG_Display": {margin: "1em 0em 0.7em;", display: "inline-block!important;"}}},
"PreviewHTML": {linebreaks: {automatic: true, width: "95% container"}}
});
MathJax.Hub.Queue(function() {
document.querySelectorAll('.MathJax').forEach(element => element.classList.add('notranslate'));
document.querySelectorAll('a.title-link, p.summary').forEach(element => element.classList.remove('notranslate'));
});
</script>
<script src="/static/MathJax-2.7.9/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
3、在延时加载的代码后面,加上如下代码:
MathJax.Hub.Queue(
['Typeset', MathJax.Hub],
function() {
document.querySelectorAll('.MathJax').forEach(element => element.classList.add('notranslate'));
document.querySelectorAll('a.title-link, p.summary').forEach(element => element.classList.remove('notranslate'));
}
);
具体效果欢迎在Cool Papers里边测试。以上方案笔者在Chrome和Safari下都测试通过,适用于Chrome自带的翻译、Safari自带的翻译以及Cool Papers自带的翻译功能。
转载到请包括本文地址:https://kexue.fm/archives/10320
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Aug. 15, 2024). 《让MathJax更好地兼容谷歌翻译和延时加载 》[Blog post]. Retrieved from https://kexue.fm/archives/10320
@online{kexuefm-10320,
title={让MathJax更好地兼容谷歌翻译和延时加载},
author={苏剑林},
year={2024},
month={Aug},
url={\url{https://kexue.fm/archives/10320}},
}
August 16th, 2024
提个建议,能不能直接给出原始arxiv链接,内嵌的PDF视图比较鸡肋。每次都是费半天劲才找到原始arxiv网站里的pdf文件,还需要通过搜索引擎。
其实已经有了,苏神在 Cool Papers 先前系列博文的评论区已经回复过啦,每篇论文标题左上角带#的数字就是 arxiv 链接。
跟@gelthin|comment-25057说的一样,这个比较隐蔽,但很抱歉实在找不到更好的入口了。如果有更好的建议(将这个链接放到哪里去),欢迎提出。
是挺隐蔽的。个人感觉更好的设计是在标题下面一行挂一排图标,把[PDF] [COPY] [KIMI]等都换成icon(详细文字变成鼠标悬停title),KIMI和arxiv就用它们网站favicon就好了,常用功能如KIMI可以把图标做大一点。
还是感觉不好排版。我再多留意下相关网站的排版,看看有没有合适的抄过来吧。
August 19th, 2024
苏神能不能再支持一下评论
类似于https://alphaxiv.org/的那种,可以对一篇论文进行一点讨论。