🦧

続・続Vivliostyle, VFM で図が使いたい

2023/02/21に公開

Vivliostyle VFM でも github と同じように mermaid の図を埋め込みたかったので、ハックしてできるように試行錯誤している(前回前々回)。さらに発展形。

今回は mermaid だけでなく plantuml も使えるようにした。もっと言うと、kroki がサポートしている言語はどれも同じように使える。

dosvg.js

  • KROKIHOST : kroki として使うホスト。https アクセスします。
  • KROKILANGS : svg として埋め込みたいコードブロックの言語をカンマ区切りで。デフォルトは mermaid
  • KROKIEMBED : data: スキームで画像を埋め込むか、KROKIHOST を使ったURLが出力されるかが切り替わる。デフォルトでは埋め込まない。

kroki で自己証明書使っている場合は、KROKIEMBED=1 を使ってください。nodejs に証明書を信用させるには NODE_EXTRA_CA_CERTS を使います。

上で書いたオプションを全部指定したコマンドラインは、例えば次のようになります。
NODE_EXTRA_CA_CERTS=server.crt KROKIEMBED=1 KROKIHOST=kroki.example.jp KROKILANGS=plantuml,mermaid NODE_OPTIONS=--require=./dosvg.js vivliostyle build input.md -o output.pdf

dosvg.js
const hostname = process.env.KROKIHOST || "kroki.io";
const langs = process.env.KROKILANGS || "mermaid";
const embed = process.env.KROKIEMBED;
const https = require("node:https");
const deasync = require("deasync");
const pako = require("pako");
const p = require("prismjs/components/prism-core");
const r = require("refractor");
const klangs = langs.split(",");

p.hooks.add("wrap", function (env) {
  if (env.type == "base64") {
    env.tag = "img";
    env.classes.push("kroki");
    env.attributes = {
      src: env.content.value
    };
    env.content.value = "";
  }
});
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)) {
    function use_kroki_direct_src() {
      const data = Buffer.from(env.code, 'utf8')
      const compressed = pako.deflate(data, { level: 9 })
      const result = Buffer.from(compressed).toString('base64').replace(/\+/g, '-').replace(/\//g, '_');
      env.code = `https://${hostname}/${env.language}/svg/${result}`;
    }
    function use_kroki_data_src(resolve) {
      const rb = {
        diagram_source: env.code,
        diagram_type: env.language,
        output_format: "svg"
      };
      const req = https.request({
        hostname: hostname,
        port: 443,
        path: "/",
        method: "POST",
      }, res => {
        let resp = [];
        res.on("data", (c) => { resp.push(c); });
        res.on("end", () => {
          const value = Buffer.concat(resp).toString("base64");
          env.code = `data:image/svg+xml;base64,${value}`
          resolve();
        });
      });
      req.write(JSON.stringify(rb));
      req.end();
    };
    if (embed) {
      deasync(use_kroki_data_src)();
    } else {
      use_kroki_direct_src();
    }
  };
});

function func(Prism) {
  klangs.forEach(e => {
    Prism.languages[e] = {
      raw: { pattern: /\A.*\z/m }
    }
  });
}
func.displayName = "kroki";
r.register(func);

Discussion