现在使用 MathJaX 直接在浏览器里显示数学公式已经非常方便了,基本上只要在 HTML header 里插入两句引用一下 MathJaX 的 CDN,然后在页面里的 LaTeX 公式就会被自动转化为数学公式显示。运作方式是 MathJaX 作为 Javascript 在客户端浏览器里运行,寻找 HTML 文档里的 LaTeX 表达式,然后将其转化为公式(通过 HTML 或者 SVG 排版的方式)显示出来。不过客户端渲染有一些小问题就是每次用户打开都要重新渲染,取决于用户的网速、系统运行 JS 的速度等等,可能渲染速度会不太一样,而且后期渲染会导致 HTML 文档布局变化等等。所以有时候考虑在 server 端预渲染是一个不错的选择。

在 MathJaX 出现之前一般 server 端的渲染是基于真实的 LaTeX 的,就是直接运行 LaTeX 编译出 dvi 文件,然后用 dvipng 之类的工具转化为图片格式,插入到 HTML 文档中。这种方式的好处是和 LaTeX 完全兼容,坏处一个是需要运行 shell 去调用 LaTeX,如果是用户输入的文本的话可能有一定安全隐患,而且对 server 负担可能也有点大,当然最重要的还是 png 这类的图片格式显示的公式特别是在 inline 的时候不能很好地和周围的文本对齐,经常会很难看。

另一个就是把 MathJaX 在 server 端预先渲染成 SVG 格式。实际上我这个博客里之前的公式一直是这样显示的,这样可以整个页面静态加载,即使客户端禁用了 Javascript 也能显示,再复杂一些也许可以做到在支持 SVG 的 RSS Reader 里也能直接显示公式。我使用的是一个叫做 svgtex 的工具。它的运行方式是通过 PhantomJS 在 server 端跑起 MathJaX 来,然后在一个端口监听,你可以通过发送 HTTP 请求到该端口,输入 LaTeX 的公式,它就会调用 MathJaX 进行渲染,然后把渲染的结果(SVG 格式)发送回来。由于我的博客是编译成静态页面的,所以我直接做了一个简单的后期处理,拿到 SVG 之后替换掉 LaTeX 公式文本内嵌到 HTML 中即可(因为 SVG 其实是文本格式),我用类似于下面这一段代码来做后期处理:

  1. require 'uri'
  2. require 'net/http'
  3. def tex2svg_impl(math, display=false)
  4. # undo some preprocessing done by pandoc
  5. require 'cgi'
  6. math = CGI.unescapeHTML(math)
  7. uri = URI.parse('http://localhost:16000')
  8. http = Net::HTTP.new(uri.host, uri.port)
  9. request = Net::HTTP::Post.new(uri.request_uri)
  10. if display
  11. request.body = '\displaystyle{' + math + '}'
  12. else
  13. request.body = math
  14. end
  15. http.request(request).body
  16. end
  17. desc "convert MathJaX formula to embeded SVG"
  18. task :tex2svg do
  19. require 'find'
  20. # currently do not support rss.xml, because
  21. # 1. pandoc do HTML escape, which is escaped again in XML, making
  22. # it difficult to unescape
  23. # 2. the SVG image is displayed as crazily full page-width images
  24. # in Firefox RSS view
  25. Find.find('compiled-production') do |path|
  26. if path.match(/html\Z/)
  27. puts path
  28. content = File.read(path)
  29. content.gsub! /<span class="math[^"]*">(.*?)<\/span>/m do |match|
  30. # strip \( \) or \[ \]
  31. math = $1[2..-3]
  32. if $1[1] == '('
  33. '<span style="font-size:100%; display:inline-block;">' +
  34. tex2svg_impl(math) +
  35. '</span>'
  36. else
  37. '<span class="svg-math-block">' +
  38. tex2svg_impl(math, true) +
  39. '</span>'
  40. end
  41. end
  42. File.open(path, 'w') do |f|
  43. f.write(content)
  44. end
  45. end
  46. end
  47. end

不过我的 Blog 代码也是很多年前写的了,最近换新电脑的时候做系统迁移才发现原来 PhantomJS 都已经停止开发了,然后 svgtex 也已经 depreciated 了……不过 svgtex 有一个后继者叫做 mathoid,看了一下现在已经是 Wikimedia 的项目了,看来 Wikipedia 现在已经是用这个在做 server 端渲染了,居然一直都没有注意到。

这个 mathoid 最开始是从 svgtex fork 出来的,使用方法基本差不多,不过不再依赖于 PhantomJS,而是基于 node.js。但是我对 node.js 并不是很熟悉,感觉 setup 又会有多一套系统需要维护,由于我现在写 blog 的频率并不高,生成博客的代码维护则更是几乎没有在跟进,这次在修理 Rack 更新之后出现的问题花了很久,发现我都几乎不太会写 Ruby 代码了,越发意识到这种没有积极维护的系统还是依赖越少,功能越简单越好,而且 server side 渲染对我来说其实并不是一定要有的功能,所以最后干脆又切换回了直接使用 MathJaX 的 CDN 在客户端渲染的方式。

不过换回去之前想把这个解决方案在这里分享一下,有可能以后会有用,而且之前也有人问过我 blog 里的公式是怎么显示的,因为右键并不会弹出 MathJaX 的菜单哈哈。不过这里还值得一提的是一个后来出现的叫做 KaTeX 的 MathJaX 的竞争者,它是直接支持 server 端渲染的,如果你有一个 server 端 javascript 系统(比如 node.js),可以直接这样渲染:

var html = katex.renderToString("c = \\pm\\sqrt{a^2 + b^2}", {
    throwOnError: false
});
// '<span class="katex">...</span>'

当然 KaTeX 和 MathJaX 相比各有优缺点,KaTeX 的优势是速度比 MathJaX 快很多(虽然 MathJaX 对于普通使用来说已经不慢了),不过相比就是支持的 TeX 命令可能没有后者那么全,虽然这些年来已经改进了很多了,不过似乎有些复杂的公式显示还是和 MathJaX 渲染的结果有一些差距。网上有一些比较详细的比较,比如 这个页面,具体用哪个还是看自己的需求啦。

2023.02.18 更新:将博客的公式系统更新到了 KaTeX,并使用 pandoc-katex 来进行服务端渲染。