Astroブログに吹き出し機能を追加!吹き出しプラグイン実装

Astroブログに吹き出し機能を追加!吹き出しプラグイン実装
記事内に商品プロモーションを含む場合があります

ブログを書く際に、以下のような吹き出しコンテンツを使いたくなる場面はないでしょうか?

吹き出しコンテンツの画像

自分は技術ブログで使うことはほぼないですが、ブログでレビュー記事などを書いている中で、吹き出しコンテンツを使いたくなる場面がありました。しかし、デフォルトの Markdown の記法の中で上記のような吹き出しコンテンツを表現する記法がなかったため独自の吹き出し記法を追加するために Remark プラグインを作成しました

本記事では、その吹き出しプラグインについてまとめています

プラグインではなくコンポーネントで吹き出しコンテンツを表示する方法も別途解説しているので、興味があれば読んでみてください!

Remarkとは

Remark とは Markdown を unist を拡張した mdast と呼ばれる抽象構文木として扱うプラグインのエコシステムのことです

unist や mdast などについて詳しくは別記事で触れているのでそちらをご覧ください!

今回は Markdown で吹き出しコンテンツを表現するために Remark プラグインを実装していきます

Markdown の記法を決める

まず、Markdown の記法を決めます。今回は Remark プラグインである remark-directive でディレクティブ記法を導入して使っていきたいと思います。ディレクティブ記法については、公式の README でも紹介されている CommonMark のドキュメントが参考になりますが、簡単に説明すると主に以下3つの記法が使えるようになります

Markdown
:textDirective

::leafDirective

:::containerDirective

今回は、ディレクティブ記法の中の leafDirective を利用して、以下の記法で吹き出しコンテンツの HTML を表現できるようにしたいと思います

Markdown
:::bubble
man Astroブログに吹き出し機能を追加!吹き出しプラグイン実装
:::

:::bubble
woman Astroブログに吹き出し機能を追加!吹き出しプラグイン実装
:::

HTML
<div class="bubble left">
  <img src="/images/bubble-man.png" alt="bubble-man" width="80px" height="80px" loading="lazy">
  <p>Astroブログに吹き出し機能を追加!吹き出しプラグイン実装</p>
</div>

<div class="bubble right">
  <p>Astroブログに吹き出し機能を追加!吹き出しプラグイン実装</p>
  <img src="/images/bubble-woman.png" alt="bubble-woman" width="80px" height="80px" loading="lazy">
</div>

remark-directive を導入する

Markdown の記法が決まったので、プロジェクトにディレクティブ記法を導入するために remark-directive プラグインをインストールします

terminal
$ yarn add remark-directive

プラグインがインストールできたら、Astro プロジェクトの astro.config.mjs ファイルでプラグインを読み込みます

astro.config.mjs
import remarkDirective from "remark-directive";
import { defineConfig } from "astro/config";

// https://astro.build/config
export default defineConfig({
  markdown: {
    remarkPlugins: [remarkDirective],
  },
})

導入ができたら、README の myRemarkPlugin を参考に試しにプラグインを作成してディレクティブ記法が使えるようになったか確かめてみます。今回は以下のような remarkBubbleContent というプラグインを作成しました

src/plugins/remarkBubbleContent.js

import { visit } from "unist-util-visit";
import { h } from 'hastscript'

export default function remarkBubbleContent() {
  return function (tree) {
    visit(tree, function (node) {
      if (
        node.type === 'containerDirective' ||
        node.type === 'leafDirective' ||
        node.type === 'textDirective'
      ) {
        console.log(node)
        const data = node.data || (node.data = {})
        const hast = h(node.name, node.attributes || {})

        data.hName = hast.tagName
        data.hProperties = hast.properties
      }
    })
  }
}

このプラグインを astro.config.mjs ファイルで読み込んで、mdx ファイルで以下のようにディレクティブ記法を使ってみます

Markdown
:text

::leaf

:::container
containerDirectiveのテスト
:::

コンソール or ターミナルに以下のような mdast が表示されていたら、remark-directive の導入は完了です

{
  type: 'textDirective',
  name: 'text',
  attributes: {},
  children: [],
  position: {
    start: { line: 1, column: 1, offset: 0 },
    end: { line: 1, column: 6, offset: 5 }
  }
}

{
  type: 'leafDirective',
  name: 'leaf',
  attributes: {},
  children: [],
  position: {
    start: { line: 3, column: 1, offset: 7 },
    end: { line: 3, column: 7, offset: 13 }
  }
}

{
  type: 'containerDirective',
  name: 'container',
  attributes: {},
  children: [ { type: 'paragraph', children: [Array], position: [Object] } ],
  position: {
    start: { line: 5, column: 1, offset: 15 },
    end: { line: 7, column: 4, offset: 54 }
  }
}

Remark プラグインを実装する

ではここから実際に Remark プラグインを実装していきます。最終目標は以下のような Markdown 記法で QA コンテンツの HTML を出力することです

Markdown
:::bubble
man Astroブログに吹き出し機能を追加!吹き出しプラグイン実装
:::

:::bubble
woman Astroブログに吹き出し機能を追加!吹き出しプラグイン実装
:::

HTML
<div class="bubble left">
  <img src="/images/bubble-man.png" alt="bubble-man" width="80px" height="80px" loading="lazy">
  <p>Astroブログに吹き出し機能を追加!吹き出しプラグイン実装</p>
</div>

<div class="bubble right">
  <p>Astroブログに吹き出し機能を追加!吹き出しプラグイン実装</p>
  <img src="/images/bubble-woman.png" alt="bubble-woman" width="80px" height="80px" loading="lazy">
</div>

まず、先ほどテストのために実装した remarkBubbleContent で上記の記法を試すと以下のような mdast に変換されます

Markdown
{
  type: 'containerDirective',
  name: 'bubble',
  attributes: {},
  children: [ { type: 'paragraph', children: [Array], position: [Object] } ],
  position: {
    start: { line: 2, column: 1, offset: 1 },
    end: { line: 4, column: 4, offset: 49 }
  }
}
{
  type: 'containerDirective',
  name: 'bubble',
  attributes: {},
  children: [ { type: 'paragraph', children: [Array], position: [Object] } ],
  position: {
    start: { line: 6, column: 1, offset: 51 },
    end: { line: 8, column: 4, offset: 101 }
  }
}

この mdast が目標となる HTML に変換されるように AST を変換させていきます

最終的な remarkBubbleContent プラグインは以下のようになりました

src/plugins/remarkBubbleContent.js
import { visit } from "unist-util-visit";

export default function remarkBubbleContent() {
  return function (tree) {
    visit(tree, function (node) {
      if (
        node.type === 'containerDirective' ||
        node.type === 'leafDirective' ||
        node.type === 'textDirective'
      ) {
        if (node.name !== 'bubble') return;

        const children = [];

        const classes = ["bubble"];
        const child = node.children[0];
        const text = child.children[0].value;

        if (child.type === 'paragraph' && text.match(/^man/)) {
          const textArray = text.split(' ')
          textArray.shift()
          const value = textArray.join('')
  
          children.push({
            type: "element",
            tagName: "img",
            properties: {
              src: "/images/bubble-man.png",
              alt: "bubble-man",
              width: "80px",
              height: "80px",
              loading: "lazy",
            },
            children: [],
          })
          children.push({
            type: "element",
            tagName: "p",
            properties: {},
            children: [{type: 'text', value: value}],
          })
          classes.push("left")
        }

        if (child.type === 'paragraph' && text.match(/^woman/)) {
          const textArray = text.split(' ')
          textArray.shift()
          const value = textArray.join('')
  
          children.push({
            type: "element",
            tagName: "p",
            properties: {},
            children: [{type: 'text', value: value}],
          })
          children.push({
            type: "element",
            tagName: "img",
            properties: {
              src: "/images/bubble-woman.png",
              alt: "bubble-man",
              width: "80px",
              height: "80px",
              loading: "lazy",
            },
            children: [],
          })
          classes.push("right")
        }

        node.data = {
          hName: "div",
          hProperties: {
            class: classes,
          },
          hChildren: children,
        };
      }
    })
  }
}

各タグのスタイルについては各自でお好きなように実装してください

Astro にプラグインを導入する

上記で実装した remarkBubbleContent プラグインを astro.config.mjs で読み込みます

astro.config.mjs
import remarkDirective from "remark-directive";
import remarkBubbleContent from "./src/plugins/remarkBubbleContent";
import { defineConfig } from "astro/config";

// https://astro.build/config
export default defineConfig({
  markdown: {
    remarkPlugins: [remarkDirective, remarkBubbleContent],
  },
})

読み込みが完了したら再度ページを読み込みます。すると以下のように吹き出しコンテンツが表示できます

bubble-man

Astroブログに吹き出し機能を追加!吹き出しプラグイン実装

Astroブログに吹き出し機能を追加!吹き出しプラグイン実装

bubble-man

参考資料

Recommend
こんな記事も読まれています!