0%

添加对 TikZ 的支持并修改配置

过年那两天尝试在网站上放一个蛇引理的交换图热热手, 结果发现 MathJax 没有把 Pgf 和 TikZ 写进去, 所以不能用 tikz-cd 画交换图. 自带的 ams-cd 又只能画水平和竖直的箭头, 约等于什么都画不了. 那怎么办呢?

上网搜了一下, The Stacks Project 用的是 XY-pic, 香蕉空间用基于 MediaWiki, 都不是我们想要的. 但是找到了一个过滤器 hexo-filter-tikzjax1, 看起来不错. 在此之前我又安装了 hexo-filter-mathjax 并把 NexT 主题配置文件中的 math.mathjax.enable 改回了 false. 直接把香蕉空间蛇引理2的源码拔过来, 放在一个 tikz 代码块里, 试用出来的结果如下:

ker0kerker00X0XX0000Y0YY00coker0cokercoker00fguv±

有两个问题: 第一个是太小了, 第二个是颜色不对. 还好这两个问题都不难解决: 第一个问题只需把 svg 文件的 widthheight 都变成原来的 1.5 倍即可, 不合适的做法是直接在头部的 <style>.tikzjax{...}</style> 中添加 transform: scale(1.5), 也就是直接修改 hexo-filter-tikzjax/dist/common.js 中的 export.defaultConfig.inline_style (或者主文件夹 _config.yml 中的 tikzjax 设置? ), 因为这会造成一些不好的显示问题. 第二个问题查看 svg 文件可以发现: 前者的 strokefill 都是 currentColor, 这里是 #555; 而后者的 stroke 是纯黑色 #000. 这个不难改, 但是直接在开头改完发现 还是黑色, 这看起来是因为希腊字母特殊渲染自带了一个 fill="#000". 这个应该只需要把所有的 #000 替换成 currentColor 就行.

为了还能显示原来的效果, 我把原来识别 tikz 代码块的部分改成了识别 tikztikzcd 代码块, 使用的占位符也做了修改, 从而可以被后续插入 svg 部分的正则表达式认出来不同, 进而应用上面的变化. 具体来讲, 将 hexo-filter-tikzjax/dist/render-tikzjax.js 修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderTikzjax = void 0;
const node_tikzjax_1 = __importDefault(require("node-tikzjax"));
const common_1 = require("./common");
const queue_1 = __importDefault(require("./queue"));
/**
* Extract TikZ code blocks from post content and render them into SVGs with `node-tikzjax`.
* The generated SVGs are saved in cache with a hash as their keys for better performance.
*/
async function renderTikzjax(data) {
const config = this.config.tikzjax;
if (!data.tikzjax && !config.every_page) {
return;
}
// Set up loggers.
const logPrefix = '[hexo-filter-tikzjax]';
const debug = (...args) => this.log.debug.apply(this.log, [logPrefix, ...args]);
const error = (...args) => this.log.error.apply(this.log, [logPrefix, ...args]);
queue_1.default.setLogger({ debug, error });
// Find all TikZ and TikZ-cd code blocks in Markdown source.
const regex = /```tikz(cd)?\n([\s\S]+?)```/g;
const matches = data.content.matchAll(regex);
for await (const match of matches) {
// Generate a hash for each TikZ code block as its cache key.
const hash = (0, common_1.md5)(JSON.stringify(match[0]) + JSON.stringify(config));
let svg = common_1.localStorage.getItem(hash);
if (!svg) {
const input = match[2]?.trim(); //match[0]=full string, match[1]=empty or cd, match[2]=tex codes
if (!input) {
continue;
}
// Since `node-tikzjax` does not allow concurrent calls,
// we have to use a task queue to make sure that only one call is running at a time.
// This could be a bottleneck when generating a large number of posts.
debug('Processing TikZ graphic...', hash);
svg = await new Promise((resolve) => {
queue_1.default.enqueue(async () => {
const svg = await (0, node_tikzjax_1.default)(input, {
showConsole: this.env.debug,
...config.tikzjax_options,
});
resolve(svg);
});
});
if (svg) {
common_1.localStorage.setItem(hash, svg);
debug('TikZ graphic saved in cache.', hash);
}
else {
debug('TikZ graphic not generated. Skipped.', hash);
}
}
else {
debug('TikZ graphic found in cache. Skip rendering.', hash);
}
// Replace the TikZ or TikZ-cd code block with a placeholder
// so that we can insert the SVG later in `insertSvg` function.
if (match[1]) {
const placeholder = `<!-- tikzjax-cd-placeholder-${hash} -->`;
data.content = data.content.replace(match[0], placeholder);
}
else {
const placeholder = `<!-- tikzjax-placeholder-${hash} -->`;
data.content = data.content.replace(match[0], placeholder);
}
}
return data;
}
exports.renderTikzjax = renderTikzjax;

再把 insert-svg.js 修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.insertSvg = void 0;
const common_1 = require("./common");
/**
* Insert generated SVGs into HTML of the post as inline tags.
*
* We separate this function from `renderTikzjax` and run them in different filters,
* since we need the Markdown source to render TikZ graphics (in `before_post_render`).
* But insert SVGs into Markdown source will cause problems, so we wait until the Markdown
* source is rendered into HTML, then insert SVGs into HTML (in `after_render:html`).
*
* Since we need to process archive/tags/categories pages too, if they contains posts which
* have TikZ graphs in it, the `after_post_render` filter is not sufficient.
*/
function insertSvg(html, locals) {
const config = this.config.tikzjax;
const page = locals.page;
const indexContains = page.__index && page.posts.toArray().find((post) => post.tikzjax);
// Only process if a post contains TikZ, or it's an index page and one of the posts contains TikZ.
if (!page.tikzjax && !config.every_page && !indexContains) {
return;
}
// Prepend CSS for TikZJax.
html = html.replace(/<head>(?!<\/head>).+?<\/head>/s, (str) => str.replace('</head>', `<link rel="stylesheet" type="text/css" href="${config.font_css_url}" />` +
(config.inline_style ? `<style>${config.inline_style}</style>` : '') +
'</head>'));
// Find all TikZ placeholders inserted by `renderTikzjax`.
const regex = /<!-- tikzjax-(cd-)?placeholder-(\w+?) -->/g;
const matches = html.matchAll(regex);
const debug = (...args) => this.log.debug('[hexo-filter-tikzjax]', ...args);
for (const match of matches) {
const hash = match[2]?.trim();
if (!hash) {
continue;
}
const svg = common_1.localStorage.getItem(hash);
debug('Looking for SVG in cache...', hash);
if (svg) {
if (match[1]) {
const svg_cd = svg.replace('g stroke="#000"', 'g stroke="#000" fill="#000"')
.replaceAll('#000', 'currentColor')
.replace(/\b(width|height)=["']([\d.]+)["']/g, (match, attr, value) => {
const scaledValue = parseFloat(value) * 1.5;
return `${attr}="${scaledValue.toFixed(3)}"`;
})
html = html.replace(match[0], `<p><span class="tikzjax">${svg_cd}</span></p>`);
debug('SVG commutative diagram inserted!', hash);
}
else {
html = html.replace(match[0], `<p><span class="tikzjax">${svg}</span></p>`);
debug('SVG inserted!', hash);
}
}
else {
debug('SVG not found in cache. Skipped.', hash);
}
}
return html;
}
exports.insertSvg = insertSvg;

于是现在只需要使用 tikzcd 代码块就能画出想要的交换图:

ker0kerker00X0XX0000Y0YY00coker0cokercoker00fguv±

本来以为事情到这里就结束了, 结果经过仔细观察, 这字符的间距是不是不太对? 它好像把我的 ker 拆成了 k-er, 把我的 coker 拆成了 cok-er, 所以 k 和 e 的间距变小了一点. 这是不能容忍的, 必须要出重拳!

查看 hexo-filter-tikzjax 的源代码, 发现生成部分主要调用的是作者的下层包 node-tikzjax.default, 查看 node-tikzjax/disc/index.js:

1
2
3
4
5
6
7
8
9
10
// row 18:
const dvi2svg_1 = require("./dvi2svg");
// row 24-30:
async function tex2svg(input, options) {
await (0, bootstrap_1.load)();
const dvi = await (0, bootstrap_1.tex)(input, options);
const svg = await (0, dvi2svg_1.dvi2svg)(dvi, options);
return svg;
}
exports.default = tex2svg;

dvi2svg.js 里看了看:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// row 5:
const dvi2html_1 = require("@prinsss/dvi2html");
// row 16-27:
let html = '';
const dom = new jsdom_1.JSDOM(`<!DOCTYPE html>`);
const document = dom.window.document;
async function* streamBuffer() {
yield Buffer.from(dvi);
return;
}
await (0, dvi2html_1.dvi2html)(streamBuffer(), {
write(chunk) {
html = html + chunk.toString();
},
});

核心又是另一个库 @prinsss/dvi2html 中的 dvi2html, 去看 /lib/index.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// row 59-75
function dvi2html(dviStream, htmlStream) {
return __awaiter(this, void 0, void 0, function () {
var parser, machine;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
parser = papersize_1.default(svg_1.default(psfile_1.default(ps_1.default(color_1.default(parser_1.mergeText(parser_1.dviParser(dviStream)))))));
machine = new html_1.default(htmlStream);
return [4 /*yield*/, parser_1.execute(parser, machine)];
case 1:
_a.sent();
return [2 /*return*/, machine];
}
});
});
}
exports.dvi2html = dvi2html;

其中连续调用了一长串函数, 看着怪吓人的.

直接编译 tex 得到的 dvi 确实把 ker 和 coker 拆开了, 所以是 parser_1.mergeText 的问题吗? 再往后的代码实在看不懂了, 对 dvi 也不熟, 就这样吧.

1. https://github.com/prinsss/hexo-filter-tikzjax
2. https://www.bananaspace.org/wiki/%E8%9B%87%E5%BD%A2%E5%BC%95%E7%90%86