Astro でコードブロックのシンタックスハイライトをしつつタイトルも付ける
Astro 標準のシンタックスハイライトには、タイトルを付ける仕組みが存在しない。remark-code-title
という Remark のプラグインを使うことで実現できるが、シンタックスハイライトと干渉することを防ぐために、code
要素の手前に div
要素を置く形になっており、AST や最終的な HTML の構造上、コードブロック本体とタイトルに関係性がない。
# 似たようなプラグインに rehype-code-titles
もあるが、Astro の場合はプラグインとシンタックスハイライトの処理順の兼ね合いで動作しない。
構造上コードとタイトルが結びつくようにすべく、標準のシンタックスハイライトを無効化して自前でシンタックスハイライトを実装した。
と言っても、シンタックスハイライト自体は Astro のコンポーネントとして提供されているので、そんなに難しいことはしていない。コードとタイトルをひとまとめで Astro のコンポーネントに置換する Remark プラグインを実装し、あとは Astro のコンポーネントの世界で Shikiji を呼び出しつつ HTML を出力すればよい。
今回は、figure
要素でラップして figcaption
要素でタイトルを付ける形の HTML を出力した。
yarn add --dev @types/mdast
import type { Code, Paragraph } from "mdast"import type { Plugin } from "unified"import type { Node } from "unist"import { visit, type Visitor } from "unist-util-visit"
const remarkCodeBlock: Plugin = () => { return (tree: Node) => { const visitor: Visitor<Code> = (node, index, parent) => { if (!parent || index === undefined) { return }
const { lang, meta, value } = node const title = meta
parent.children.splice(index, 1, { type: "paragraph", data: { hName: "code-block", hProperties: { // Workaround for preventing loss of line breaks in code blocks. // Need to decode the value in the code block component. code: encodeURIComponent(value), ...(lang ? { lang } : {}), ...(title ? { title } : {}), }, }, children: [], } as Paragraph) } visit(tree, "code", visitor) }}
export default remarkCodeBlock
import mdx from "@astrojs/mdx"import { defineConfig } from "astro/config"
import remarkCodeBlock from "./src/remark/remark-code-block"
export default defineConfig({ integrations: [ mdx(), ], markdown: { syntaxHighlight: false, // default false なので実際には書かなくてよい remarkPlugins: [ remarkCodeBlock, ], },})
---import { Code } from "astro:components"import { type BuiltinLanguage, type SpecialLanguage } from "shikiji"
interface Props { lang?: string title?: string code: string}
const { lang, title, code } = Astro.props---
<figure> {title && <figcaption>{title}</figcaption>} <Code lang={lang as BuiltinLanguage | SpecialLanguage | undefined} code={decodeURIComponent(code)} theme="min-light" /></figure>
---import CodeBlock from "../components/CodeBlock.astro"
const { Content } await entry.render()---
<Content components={{ "code-block": CodeBlock, }}/>
あとは、Markdown を書くときに以下のように言語名の後ろにスペースを挟んでタイトルを書けばよい (シンタックスハイライトを使わずにタイトルだけ付けたい場合には言語を plaintext
とする)。
```astro title=hello.astro------
<p>Hello, world!</p>```
この例は以下のようにレンダリングされる。
------
<p>Hello, world!</p>
ちなみに、Markdown の AST 仕様である mdast では、言語名の後ろにスペースを挟んで書かれた文字列は meta
として保持されることになっている。今回は meta
全体をそのままタイトルとして使ったものの、他にも情報を含めておいてレンダリングに活用することも考えられる。