我们注意到 Snacks.nvim 中的 image 功能模块以更快的加载速度 和一站式集成的体验胜过了 image.nvim。 而这两者,或者甚至可以包括 LaTeX 在内,对中文的支持始终让人相当头疼。 本文试图给出一种能在公式中使用中文的方案。

结论先行

参考配置如下:

{
	"folke/snacks.nvim",
	-- 中略
	opts = {
        -- 中略
		image = {
			enabled = true,
			-- 中略
            math = {
                enabled = true,
                latex = {
                    packages = {"amsmath", "amssymb", "amsfonts", "amscd", "mathtools", 
                                "fontspec", "xeCJK"},
                    tpl = [[
                        \documentclass[preview, border = 0pt, varwidth, 12pt]{standalone}
                        \usepackage{${packages}}
                        \setCJKmainfont{Noto Serif CJK SC}
                        \begin{document}
                        ${header}
                        { \${font_size} \selectfont
                            \color[HTML]{${color}}
                        ${content}}
                        \end{document}
                    ]],
                },
            },
		},
	},
    -- 后略
},

你需要安装一个版本尽可能新的 tectonic 以使其按预期发挥作用。

详细分析

我们注意到 Snacks.nvim 官方默认配置 中 是这样描述这一段的:

math = {
    enabled = true, -- enable math expression rendering
    -- in the templates below, `${header}` comes from any section in your document,
    -- between a start/end header comment. Comment syntax is language-specific.
    -- * start comment: `// snacks: header start`
    -- * end comment:   `// snacks: header end`
    typst = {
      tpl = [[
        #set page(width: auto, height: auto, margin: (x: 2pt, y: 2pt))
        #show math.equation.where(block: false): set text(top-edge: "bounds", bottom-edge: "bounds")
        #set text(size: 12pt, fill: rgb("${color}"))
        ${header}
        ${content}]],
    },
    latex = {
      font_size = "Large", -- see https://www.sascha-frank.com/latex-font-size.html
      -- for latex documents, the doc packages are included automatically,
      -- but you can add more packages here. Useful for markdown documents.
      packages = { "amsmath", "amssymb", "amsfonts", "amscd", "mathtools" },
      tpl = [[
        \documentclass[preview,border=0pt,varwidth,12pt]{standalone}
        \usepackage{${packages}}
        \begin{document}
        ${header}
        { \${font_size} \selectfont
          \color[HTML]{${color}}
        ${content}}
        \end{document}]],
    },
},

对于 typst 的项我们忽略,主要关注 latex 方向。 此处有一个 tpl,是生成 latex 的模板。

注意到 documentclass 被设为了 standalone, 这是为了使生成的 PDF 文件恰与公式等大,便于嵌入文件。 而前面的 preview 是为了精细裁切,border = 0pt 是为了公式边缘不留空白, varwidth 宽度自适应,12pt 指基础字号。

然后引用了上面 packages 列表中的包,此处官方默认有五个, 基本的数学符号都在里面了。

然后按注释,你可以通过在 Markdown 文件中插入

// snacks: header start
... some latex code ...
// snacks: header end

来为你的 LaTeX 公式自定义头, 例如 \let \fa \forall 一类。

建议在写文章之前就把这些 header 定义好, 因为 Snacks 的热重载频率很高, 会在你输入头输入到一半时不断重编译文中所有公式 造成风扇狂转和 tectonic 崩溃。

然后是正文部分,设置合适的字号(\selectfont 用来确保字号生效), 接着用给公式涂上主题色(SnacksImageMath), 最后是公式正文内容。

接着我们注意到如果要考虑中文支持,我们就要考虑它使用什么工具进行编译。 据 lua/snacks/image/convert.lua,我们有:

typ = {
  ft = "pdf",
  cmd = {
    {
      cmd = "typst",
      args = { "compile", "--format", "pdf", "--pages", 1, "{src}", "{file}" },
    },
  },
},
tex = {
  ft = "pdf",
  file = function(convert, ctx)
    ctx.pdf = Snacks.image.config.cache .. "/" .. vim.fs.basename(ctx.src):gsub("%.tex$", ".pdf")
    return convert:tmpfile("pdf")
  end,
  cmd = {
    {
      cwd = "{dirname}",
      cmd = "tectonic",
      args = { "-Z", "continue-on-errors", "--outdir", "{cache}", "{src}" },
    },
    {
      cmd = "pdflatex",
      cwd = "{dirname}",
      args = { "-output-directory={cache}", "-interaction=nonstopmode", "{src}" },
    },
  },
  -- 错误处理略
},

因此是有就先调用 tectonic,没有的话就去调用 pdflatex。 显然在前者中实现 CJK 支持比后者要简单。

我们在 packages 中加入 xeCJk,同时加入 fontspec 用来辅助查找字体(tectonic 查找字体能力偏弱), 并在 tpl 中加入一行 \setCJKmainfont{Noto Serif CJK SC}(替换成你电脑上实际有的字体)。

接着,见证奇迹的时刻……由此顺利结束,have fun!