技術ドキュメントやブログ記事では、読者に注意を促す「注意書き」や「ヒント」「警告」などのビジュアルコールアウトが役立ちます。
この記事では、Astro プロジェクトで remark プラグイン を利用してカスタムアドモニションを実装する方法を紹介します。

Adminitionsの必要性

注意事項、ヒント、重要な情報、警告など、特定の情報を強調するために使います。たとえば、Markdown 内で以下のように記述することで表現できます。

:::important[重要]
これはとても重要な情報です。
:::

この記法により、後述するプラグインがこの部分を解析し、HTML の <aside> 要素に変換して、適切なクラスと属性(例: data-admonition-type="important")を付与します。最終的に、SCSS で設定したスタイルにより「重要」なアドモニションは紫色で表示されます。

ライブラリのインストール

remark-directive を使用してプラグインの作成を行います。

Terminal window
yarn add remark-directive

実装

1. remarkAdmonitions プラグインの作成

まず、カスタムの remark プラグインを作成します。このプラグインは Markdown の AST(抽象構文木)を走査し、Admonition用のディレクティブを検出して、以下の処理を行います。

Markdown 内で :::tip :::important などのディレクティブを検出します。

ディレクティブの最初の子ノードにカスタムタイトルが指定されている場合、そのタイトルを抽出します。例: :::important[重要なお知らせ] と記述すると、タイトルに「重要なお知らせ」が表示されます。

ディレクティブを <aside> 要素に変換し、class="admonition"data-admonition-type 属性を付与します。また、タイトル部分は <p class="admonition-title">、本文は <div class="admonition-content"> にラップします。

src/plugins/remark-admonitions.ts
import type { AdmonitionType } from "src/types";
import type { Node, Paragraph as P, Parent, Root, PhrasingContent } from "mdast";
import type { Directives, LeafDirective, TextDirective } from "mdast-util-directive";
import { directiveToMarkdown } from "mdast-util-directive";
import { toMarkdown } from "mdast-util-to-markdown";
import type { Plugin } from "unified";
import { visit } from "unist-util-visit";
import { type Properties, h as _h } from "hastscript";
import { toString as mdastToString } from "mdast-util-to-string";
const Admonitions = new Set<AdmonitionType>(["tip", "note", "important", "caution", "warning"]);
function isAdmonition(s: string): s is AdmonitionType {
return Admonitions.has(s as AdmonitionType);
}
function isNodeDirective(node: Node): node is Directives {
return node.type === "containerDirective" || node.type === "leafDirective" || node.type === "textDirective";
}
// textDirective や leafDirective の場合の処理(ディレクティブを Markdown に変換してテキストノードへ置き換える)
function transformUnhandledDirective(node: LeafDirective | TextDirective, index: number, parent: Parent) {
const textNode = {
type: "text",
value: toMarkdown(node, { extensions: [directiveToMarkdown()] }),
} as const;
if (node.type === "textDirective") {
parent.children[index] = textNode;
} else {
parent.children[index] = {
children: [textNode],
type: "paragraph",
};
}
}
// HAST ノードを生成するためのヘルパー関数
function h(el: string, attrs: Properties = {}, children: any[] = []): P {
const { properties, tagName } = _h(el, attrs);
return {
children,
data: { hName: tagName, hProperties: properties },
type: "paragraph",
};
}
export const remarkAdmonitions: Plugin<[], Root> = () => tree => {
// AST を走査
visit(tree, (node, index, parent) => {
if (!parent || index === undefined || !isNodeDirective(node)) return;
// textDirective や leafDirective は別処理
if (node.type === "textDirective" || node.type === "leafDirective") {
transformUnhandledDirective(node, index, parent);
return;
}
// ここから containerDirective の処理
const admonitionType = node.name;
if (!isAdmonition(admonitionType)) return;
// デフォルトではタイトルにアドモニションの種類をそのまま使用
let title: string = admonitionType;
let titleNode: PhrasingContent[] = [{ type: "text", value: title }];
// カスタムタイトルが指定されているかチェック
const firstChild = node.children[0];
if (
firstChild?.type === "paragraph" &&
firstChild.data &&
"directiveLabel" in firstChild.data &&
firstChild.children.length > 0
) {
titleNode = firstChild.children;
title = mdastToString(firstChild.children);
node.children.splice(0, 1);
}
// <aside> 要素を生成(class と data 属性のみ付与)
const admonition = h(
"aside",
{
"aria-label": title,
class: "admonition",
"data-admonition-type": admonitionType,
},
[
h("p", { class: "admonition-title", "aria-hidden": "true" }, [...titleNode]),
h("div", { class: "admonition-content" }, node.children),
]
);
parent.children[index] = admonition;
});
};

2. Astro の設定にプラグインを追加

次に、astro.config.mjsに先ほどのプラグインをインポートして設定します。

astro.config.mjs
import { defineConfig } from 'astro/config';
import { remarkAdmonitions } from "./src/plugins/remark-admonitions";
export default defineConfig({
markdown: {
remarkPlugins: [remarkAdmonitions],
},
});

これで、Astro は Markdown の変換時に remarkAdmonitions プラグインを実行し、Admonitionを生成します

3. SCSS でアドモニションのスタイルを定義

生成された <aside class="admonition" data-admonition-type="important"> などの要素に対して、SCSS でスタイルを適用します。以下は、タイトル部分の左側に SVG アイコンを配置し、タイトルテキストとアイコンの色を border と同じにする例です。

/* --- Admonition の基本スタイル --- */
.admonition {
border-left: 5px solid;
padding: 16px;
margin: 1em 0;
p {
margin: 0;
margin-top: 8px;
}
}
/* admonition-title の左側にアイコンを配置するためのスタイル */
.admonition .admonition-title {
position: relative;
padding-left: 32px;
font-size: 20px;
font-weight: bold;
margin: 0;
}
.admonition .admonition-title::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
display: inline-block;
/* mask プロパティで SVG のシルエットを currentColor で表示 */
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
-webkit-mask-size: contain;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
background-color: currentColor;
}
/* --- 各 admonition タイプごとのスタイル --- */
/* Tip */
.admonition[data-admonition-type="tip"] {
border-color: #28a745;
background-color: #f3fff3;
.admonition-title {
color: #28a745;
}
}
.admonition[data-admonition-type="tip"] .admonition-title::before {
mask-image: url("/icons/tip.svg");
-webkit-mask-image: url("/icons/tip.svg");
}
/* Note */
.admonition[data-admonition-type="note"] {
border-color: #007acc;
background-color: #f4faff;
.admonition-title {
color: #007acc;
}
}
.admonition[data-admonition-type="note"] .admonition-title::before {
mask-image: url("/icons/note.svg");
-webkit-mask-image: url("/icons/note.svg");
}
/* Important */
.admonition[data-admonition-type="important"] {
border-color: #800080;
background-color: #fef6ff;
.admonition-title {
color: #800080;
}
}
.admonition[data-admonition-type="important"] .admonition-title::before {
mask-image: url("/icons/important.svg");
-webkit-mask-image: url("/icons/important.svg");
}
/* Caution */
.admonition[data-admonition-type="caution"] {
border-color: #fd7e14;
background-color: #fff9f0;
.admonition-title {
color: #fd7e14;
}
}
.admonition[data-admonition-type="caution"] .admonition-title::before {
mask-image: url("/icons/caution.svg");
-webkit-mask-image: url("/icons/caution.svg");
}
/* Warning */
.admonition[data-admonition-type="warning"] {
border-color: #dc3545;
background-color: #fff2f2;
.admonition-title {
color: #dc3545;
}
}
.admonition[data-admonition-type="warning"] .admonition-title::before {
mask-image: url("/icons/warning.svg");
-webkit-mask-image: url("/icons/warning.svg");
}

まとめ

今回の記事では、Astro プロジェクト内で remark プラグインを利用して Markdown のアドモニションを実装する方法を紹介しました。

参考

  1. Added admonitions