monolithic kernel

Mermaid の図を埋め込むために Shadow DOM を使わなくてもよくなった

前回の記事では、ブログに Mermaid の図を埋め込むために Shadow DOM でラップして id 属性の被りを回避していたが、調べたところ SVG を生成する際に id を指定できるようだった。

id の命名をどうするかで少し悩んだが、とにかくユニークになれば何でもいいだろうということで、ツリーの中での mermaid 要素の位置 (index) を使うことにした。Markdown の AST を見るときにツリーに埋め込んで、カスタムコンポーネントで受け取っている。

src/remark/remark-mermaid.ts
import type { Code, Paragraph } from "mdast"
import type { Root } from "mdast"
import type { Plugin } from "unified"
import type { Node } from "unist"
import { visit, type Visitor } from "unist-util-visit"

const remarkMermaid: Plugin<[], Root> = () => {
  return async (tree: Node) => {
    const visitor: Visitor<Code> = (node, index, parent) => {
      if (!parent || index === undefined) {
        return
      }

      const { lang, meta, value } = node
      const title = meta

      if (lang !== "mermaid") {
        return
      }

      parent.children.splice(index, 1, {
        type: "paragraph",
        data: {
          hName: "mermaid",
          hProperties: {
            // Workaround for preventing loss of line breaks in mermaid.
            // Need to decode the value in the mermaid component.
            code: encodeURIComponent(value),
            ...(title ? { title } : {}),
            index, // include index as an attribute
          },
        },
        children: [],
      } as Paragraph)
    }
    visit(tree, "code", visitor)
  }
}

export default remarkMermaid
src/components/Mermaid.astro
---
import { run } from "@mermaid-js/mermaid-cli"
import * as fs from "fs/promises"
import * as os from "os"
import * as path from "path"

type Props = {
  title?: string
  code: string
  index: number
}

const renderSVG = async (mermaidContent: string, index: number): Promise<string> => {
  const prefix = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "mermaid-")), "diagram")
  await fs.writeFile(`${prefix}.mmd`, mermaidContent)
  await run(`${prefix}.mmd`, `${prefix}.svg`, {
    outputFormat: "svg",
    quiet: true,
    puppeteerConfig: {
      headless: "new",
    },
    parseMMDOptions: {
      svgId: `mermaid-${index}`, // use index
    },
  })
  return await fs.readFile(`${prefix}.svg`, "utf-8")
}

const { title, code, index } = Astro.props

const svg = await renderSVG(decodeURIComponent(code), index)
---

<figure>
  {title && <figcaption class="text-sm font-bold">{title}</figcaption>}
  <Fragment set:html={svg} />
</figure>

<style>
  figure :global(svg) {
    all: initial;
  }
</style>

Declarative Shadow DOM を使うと View Transitions を使えなくて困っていたので、Shadow DOM なしでスッキリする落とし所にできてよかった。View Transitions というか SPA モードはページ遷移が速くて気持ちがいい。


Related articles