🐒

Vivliostyle, VFM で図が使いたい

2023/02/08に公開

Vivliostyle で VFM から PDF を生成できて大変便利!……なのだけど、githubみたいにmermaidの図もmarkdownでサクサク書いて、図と文章とセットで(バラバラにならないように)管理したい。この前初めてお邪魔させていただいた開発者会議でそんなことを漏らしたら、「最近追加された JavaScript を走らせる機能でできるんじゃない?」「やった人の記事があるから、参考にするといいかも」という話をいただいた。

そうは言うものの、よくよく考えてみたら VFM レベルで処理されるべきなので、掘り進めて実装してみたらできた気がする。

How-to

一言でいうと「NODE の引数で関数をプリロードさせて、ライブラリに潜り込ませる!」です。本体への改変なしでいけました。

NODE_OPTIONS=--require=./dosvg.js vfm example.md
NODE_OPTIONS=--require=./dosvg.js vivliostyle build example.md

dosvg.js

本体はちょっと長いですが、こんな感じです。kroki を使って svg を得ています。デフォルトでは kroki.io を使いますが、自分で kroki を用意した場合は、環境変数 KROKIHOST で切り替えてください。

デフォルトでは mermaid のみを対象としていますが、他のエンジンで svg を出力したいときは、環境変数 KROKILANGS にカンマ区切りで指定してください。refractor 3.x と kroki の両方でサポートされているのは mermaid,dot です。

dosvg.js
const hostname = process.env.KROKIHOST || "kroki.io";
const langs = process.env.KROKILANGS || "mermaid";
const https = require("node:https");
const deasync = require("deasync");
const p = require("prismjs/components/prism-core");
const klangs = langs.split(",");
p.hooks.add("wrap", function (env) {
  if (env.type == "base64") {
    env.content = {
      type: "raw",
      value: Buffer.from(env.content.value, "base64").toString()
    }
  }
});
p.hooks.add("after-tokenize", function (env) {
  if (klangs.includes(env.language)) {
    env.tokens = [new p.Token("base64", env.code, null, null)];
  }
});
p.hooks.add("before-tokenize", function (env) {
  if (klangs.includes(env.language)) {
    env.grammar = { raw: { pattern: /\A.*\z/m } };
    const rb = {
      diagram_source: env.code,
      diagram_type: env.language,
      output_format: "svg"
    };
    const p = (resolve) => {
      const req = https.request({
        hostname: hostname,
        port: 443,
        path: "/",
        method: "POST",
      }, res => {
        let resp = [];
        res.on("data", (c) => { resp.push(c); });
        res.on("end", () => {
          env.code = Buffer.concat(resp).toString("base64");
          resolve();
        });
      });
      req.write(JSON.stringify(rb));
      req.end();
    };
    deasync(p)();
  };
});

Discussion