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 モードはページ遷移が速くて気持ちがいい。