Mermaid の図を埋め込むために Shadow DOM を使わなくてもよくなった
前回の記事では、ブログに Mermaid の図を埋め込むために Shadow DOM でラップして id 属性の被りを回避していたが、調べたところ SVG を生成する際に id を指定できるようだった。
id の命名をどうするかで少し悩んだが、とにかくユニークになれば何でもいいだろうということで、ツリーの中での mermaid
要素の位置 (index
) を使うことにした。Markdown の AST を見るときにツリーに埋め込んで、カスタムコンポーネントで受け取っている。
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
---
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 モードはページ遷移が速くて気持ちがいい。