🐒

続Vivliostyle, VFM で図が使いたい

2023/02/14に公開

Vivliostyle VFM でも github と同じように mermaid の図を埋め込みたかったので、ハックしてできるようにしたのが前回。あれから問題が見つかり、対応したのが今回。

dosvg.js

kroki で svg を取得する大まかな流れは変更なし。使い方も変更なし。前回は html 中に直接 svg を埋め込んでいたけれども、今回は img タグにして、data: スキームで画像として埋め込んでいる。

img タグにしたので、サイズ調整の CSS も要変更。svg の代わりに img.kroki に対して行えば ok。

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.tag = "img";
  	env.classes.push("kroki");
  	env.attributes = {
      src: `data:image/svg+xml;base64,${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)) {
    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)();
  };
});

何が問題だったか

svg は html に直接置くことができる。できるのだけれど、svg は XML なので、html に置くと、html と xml の構文が混じった状態になる。html でなく xhtml なら問題ないかもしれなけれども、html5 は xhtml から離れる方向に進んだので、文法が異なる。

vfm の処理は mdast から hast へ、hast から html へと変換を行う。hast では parse5 が使われている。ただし parse5はXML をサポートしておらず、全て html として解釈する。

mdast の段階で svg を埋め込むと、hast で svg がパースされる。svg は XML なので内部に xhtml が埋め込まれているけれども、parse5 で html として処理される。結果 svg 中の xhtml <br /> が html のようなもの <br></br> に変換されて svg に再び埋め込まれる。Chrome は svg 内の xhtml <br></br> が処理できずに空白になる、ということが起こっていた。

data: スキームを使うと、データが混ざらないので、上記の現象を回避できる。

Discussion