EY-Office ブログ

EY-OfficeサイトもJamstackブームに乗ってみようと思う(4) 〜Remarkプラグインを書いた〜

久しぶりに、EY-OfficeサイトもJamstackブームに乗ってみようと思う(1)EY-OfficeサイトもJamstackブームに乗ってみようと思う(2) の続きです。(2)で問題に成っていた「説明リスト(dl dt dd)が表現されない」を解決できました!

未来 MDN Web Docsより

説明リスト(dl dt dd)が表現されない問題

説明リスト(dl dt dd)は基本的なMarkdownにはありません、よく使われているGitHubのMarkdownでもサポートされていません。しかしJekyllで使われている kramdownにはサポートされているので、EY-Officeサイトでは以下のように使っています。

概要
: 
* 既にjQuery等でフロントエンド開発経験のあるソフトウェア技術者がReact(Redux)が理解できる
* Context, Hooks, TypeScriptなどの最新のReactに対応しています
* テスト駆動開発の基本を理解できる
* React 開発プロジェクトにプログラマーとして参加できるようになります

時間
: 
* 3日(御社に出向く場合)

内容
: 
* JavaScript 復習 (ES6を中心とした補足)
* TypeScript入門
* React入門 (Context, Hooks)
* Redux入門
* フロントエンド開発環境構築
* フロントエンドのテスト駆動開発入門
* React実習

これが以下のようなHTMLに変換されます

<dl>
  <dt>概要</dt>
  <dd>
    <ul>
      <li>既にjQuery等でフロントエンド開発経験のあるソフトウェア技術者がReact(Redux)が理解できる</li>
      <li>Context, Hooks, TypeScriptなどの最新のReactに対応しています</li>
      <li>テスト駆動開発の基本を理解できる</li>
      <li>React 開発プロジェクトにプログラマーとして参加できるようになります</li>
    </ul>
  </dd>
  <dt>時間</dt>
  <dd>
    <ul>
      <li>3日(御社に出向く場合)</li>
    </ul>
  </dd>
  <dt>内容</dt>
  <dd>
    <ul>
      <li>3日(御社に出向く場合)</li>
      <li>JavaScript 復習 (ES6を中心とした補足)</li>
      <li>TypeScript入門</li>
      <li>React入門 (Context, Hooks)</li>
      <li>Redux入門</li>
      <li>フロントエンド開発環境構築</li>
      <li>フロントエンドのテスト駆動開発入門</li>
      <li>React実習</li>
    </ul>
  </dd>
<dl>

以前のブログに書いたようにMDXで使っているMarkdown処理系、remark用プラグインremark-deflistをインストールしてみましたが<dd>の部分には単純な文しか書けない仕様で上手くいきませんでした。

MDXではReactのコンポーネントが使えるので、説明リスト用コンポーネントを作るという方法が使えます。これはReactがわかっていれば作るのは簡単ですが、上のようなmarkdown表記をコンポーネントの表記を置き換える必要がありスマートではありません。

Remarkプラグインを書いた

結局、上のようなmarkdown表記を扱えるremark用プラグインを作る事にしました。remark pluginの作り方にリンクされているCreating a plugin with unifiedなどを読みましたが、なかなか理解できませんでした。検索してもわかりやすい記事は見つけられませんでした。そこでremark-deflistの動きを調査し、改造する方向でプラグインを作り始めました。

注意: まだremark, unist, mdastを深くは理解できてないので、以下の解説、コードには間違いがあるかもしれません。

  • remarkプラグインはmarkdownのAST(Abstract Syntax Tree)を辿り(トラバースし)、ASTを書き換えてHTMLを生成します
  • 今回作ったプラグインは上のMarkdown表記のみ処理します、汎用性はありません
  • このコードで行っている事
    • unist-util-visit() を使い、ASTから list(ul, li) を取得します、そのlistの前の要素が文(paragraph):の場合のみ処理
    • :list から <dl><dt>文</dt>,<dd>list(ul, li)</dd></dl>のHTMLに変換します
    • dt, ddが連続している場合は最初のdlの子要素に追加する
    • 不用になった要素は削除、また次に辿る場所を更新する
/**
 * Remark Description list plugin.
 *
 * suported syntax

Term 1
:
* Definition 11
* Definition 12

Term 2
:
* Definition 21
* Definition 22

 */

const visit = require('unist-util-visit')

const isDefList = (node, i, parent) =>
  i > 0 &&
  node.type === 'list' &&
  parent.children[i - 1].type === 'paragraph' &&
  /:$/.test(parent.children[i - 1].children[0].value)

const inDefListGroup = (i, parent) =>
  i > 1 &&
  parent.children[i - 2].type === 'descriptionlist'

const mkTag = (type, tag, children) => ( {
  type, data: { hName: tag }, children} )

function descriptionUnorderedList (_options = {}) {
  return (tree, _file) => {
    visit(tree, ['list'], (node, i, parent) => {
      if (!isDefList(node, i, parent)) {
        return
      }

      const term = parent.children[i - 1].children[0]
      term.value = term.value.replace(/\n:$/, '')

      const descriptionChildIx = inDefListGroup(i, parent) ? i - 2 : -1
      const descriptionChild = descriptionChildIx >= 0 ?
        parent.children[descriptionChildIx] :
        mkTag('descriptionlist', 'dl', [])

      descriptionChild.children.push(
        mkTag('descriptionterm', 'dt', parent.children[i - 1].children))
      descriptionChild.children.push(
        mkTag('descriptiondetails', 'dd',
        [mkTag('unorderedlist', 'ul', node.children)]))

      if (descriptionChildIx >= 0) {
        parent.children[descriptionChildIx] = descriptionChild
        parent.children.splice(i - 1 , 2)
        return [true, i - 2]
      } else {
        parent.children.splice(i - 1, 2, descriptionChild)
        return undefined
      }
    })
  }
}

module.exports = descriptionUnorderedList

これにより、EY-OfficeサイトのJamstack化も一歩進みました 😀

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ