flexmark-javaでmarkdownで記事管理をする(classとか属性も付与できる)

セレスアドベントカレンダー24日目です。

セレスでWebエンジニアをやっている@ko-sasakiです。不動産や注文住宅系のメディアのサービス開発を行っています。当メディアはよくあるWordpressではなく、独自で記事管理システムを作っています。記述はHTMLでもマークダウンでもいいのですが、マークダウンを推奨しています。これは記事の可読性や編集等をしやすく狙いがあります。マークダウンで入力されたものをHTML等に出力するために、システム内でflexmark-javaを結構使っているので、その紹介をさせていただきます。

<a href="https://mochiie.com/">持ち家計画</a>
<img src="https://qiita-image-store.s3.amazonaws.com/0/228508/00ddd971-b38e-6d0e-ee9e-d15fea0c86e3.png" width="20%" />

<a href="https://oh-ya.jp/">不動産投資Oh!Ya</a>
<img src="https://qiita-image-store.s3.amazonaws.com/0/228508/69aa1b4a-3923-8c8d-d2ee-3e9ed138fd90.png" width="20%" />

## Javaのマークダウンパーサたち
Javaもいくつかマークダウンパーサの実装があって検討しました。

- commonmark-java(https://github.com/atlassian/commonmark-java)
アトラシアンがメンテしているマークダウンパーサです。[CommonMark](https://commonmark.org/)というマークダウン仕様を実装しているものになります。CommonMarkは他にもPerlやRubyなどの複数の言語で実装されています。拡張ポイントがあるので、本体をさわることなく機能拡張はできそう。

- markedj(https://github.com/gitbucket/markedj)
gitbucket製のマークダウンパーサ。あんまり活発ではなさそうで、拡張機能とかもなさそうでした。

- flexmark-java(https://github.com/vsch/flexmark-java)
今回使用したマークダウンパーサで、pagdown,kramdown,commonMarkなどにも対応しているもようです。開発も結構活発で、最近Java9のmodule対応とかもしていました。またPDFやDOCXなどにも出力とかできるので、簡単な帳票出力のテンプレートとしても使えそうでした。拡張機能もあり、本体のソースコードをさわることなく、拡張が可能です。また性能もそこそこよく、10万文字くらいまでは普通に変換されます。(一部パーサでは2万文字くらいで落ちたりしていました)

調べたときは色々あったんですけど、結構`Deprecated`になっていたので、割愛しております。

## 設定と出力

マークダウンパーサはとても簡単でオプション設定して、マークダウンをパーサにかけます。
出力後はcom.vladsch.flexmark.ast.Document型になります。HTMLやPDFの変換はその後に行います。

べた書きするとこんな感じのコードになります。

```

String source = "## title"

MutableDataSet option = new MutableDataSet();
option.setFrom(ParserEmulationProfile.MULTI_MARKDOWN)
      .set(Parser.EXTENSIONS, Arrays.asList(
           AbbreviationExtension.create() // エイリアスまわりの拡張
           , DefinitionExtension.create() // 定義まわりの拡張
           , TablesExtension.create()  // テーブルまわりの拡張
           , TocExtension.create()     // 目次まわりの拡張
           , TypographicExtension.create() 
           , AttributesExtension.create()　// タグ内の属性追加の拡張
     ));

Parser parser = Parser.builder(option).build();  // パーサーにオプションを付与する
Document doc = parser.parse(source); // markdownをパースしてDocument型に変換する
HtmlRenderer renderer = HtmlRenderer.builder(option).build(); // HTMLに変換するオプションを指定する
String html = renderer.render(doc)  // Document型のデータをHTMLに変換する
```

## サンプル

H2〜H4タグや、リストタグ、テーブルタグ、aタグなど普通のマークダウンと同じ書き方でちゃんと出力できます。

変換前

```
## H2
リスト
- 1
- 2
- 3

### H3
リスト
+ 1
+ 2
+ 3

#### H4
オーダーリスト
1.
1.
1.

##### リンク
[google](https://google.com)

##### テーブル
|head1|head2|
|----|----|
|contents1|contents2|
```

変換後

```
<h2 id="h2">H2</h2>
<p>リスト</p>
<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>
<h3 id="h3">H3</h3>
<p>リスト</p>
<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>
<h4 id="h4">H4</h4>
<p>オーダーリスト</p>
<ol>
  <li></li>
  <li></li>
  <li></li>
</ol>
<h5 id="リンク">リンク</h5>
<p><a href="https://google.com">google</a></p>
<h5 id="テーブル">テーブル</h5>
<table>
  <thead>
    <tr>
      <th>head1</th>
      <th>head2</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>contents1</td>
      <td>contents2</td>
    </tr>
  </tbody>
</table>
```

と簡単な紹介だとこんな感じになります。

## markdownに属性を付与する
マークダウンは一般的にはclassとかstyleとか付与できません。とはいえ、Webデザインではclass指定とかここだけstyleしたいとかそういうのは、日常茶飯事かと思います。flexmark-javaだと、*AttributesExtension*があり、属性情報を追加で付与することができます。

### 記法
普通にマークダウンを書き、末尾に`{属性=値}`を記述します。

```
[通常]
[google](https://google.com)
↓
<p><a href="https://google.com">google</a></p>

[属性付与]
[google](https://google.com){target="_blank"}
↓
<p><a href="https://google.com" target="_blank">google</a></p>
```

上記のとおり、通常だと、普通のaタグですが、`{}`で属性を付与することができます。これは基本どこでも使用でき、不動産投資Oh!Ya、持ち家計画のサイト内でもいたるところに使われています。また、テーブル等で一部のセルだけ強調表示したいときにも使います。

持ち家計画の例です。
下記のコードを書くと、そのセルにだけclassがあたります。これでライターの要望にも応えています。なお、行に対しては当てられません。

```
|都道府県名|建築費＋土地代|
|---|---|
|北海道|3,226万円|
|青森県|3,156万円|
|{class="ptc-Table_Highlight_Blue"}岩手県|{class="ptc-Table_Highlight_Blue"}3,154万円|
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/228508/1840a07a-60ea-70ee-3d7d-c8581c2caa36.png)

## 目次(TOC,table of contens)を自動生成する
サーバ側で目次の自動生成まで行います。Javascriptで実装してもよかったんですが、生成までに時間がかかるのと、記事によって目次をつけたい、つけたくないみたい要望に応えるためにサーバ側で実装することにしました。flexmark-javaの`TocExtension`を使用します。

```
option.set(TocExtension.DIV_CLASS, "pst-Table_Contents"); // class指定
        option.set(TocExtension.TITLE_LEVEL, 32);   // 見出しの大きさ
        option.set(TocExtension.TITLE, "目次");    　　　// 見出しの文言
        option.set(TocExtension.LEVELS, 4 | 8 | 16 );  // H2〜H4まで目次生成する
```

これをセットします。
これでマークダウンのテキストに`[TOC]`と書けば、その場所に目次が生成されます。

どこでもいいので、`[TOC]`と書くだけで、`H2〜H4`のタグの目次が自動生成されます

```
[TOC]
## H2-1
### H2-1-H3
## H2-2
### H2-2-H3


```

変換後は下記になります。
```
<div class="pst-Table_Contents">
  <h6>目次</h6>
  <ul>
    <li>
      <a href="#h21">H2-1</a>
      <ul>
        <li>
          <a href="#h21h3">H2-1-H3</a>
          <ul>
            <li><a href="#h21h3">H2-1-H3</a></li>
          </ul>
        </li>
      </ul>
    </li>
    <li>
      <a href="#h22">H2-2</a>
      <ul>
        <li>
          <a href="#h22h3">H2-2-H3</a>
          <ul>
            <li><a href="#h22h3">H2-2-H3</a></li>
          </ul>
        </li>
      </ul>
    </li>
  </ul>
</div>
```
これで目次が生成されます。不動産投資Oh!Yaではスタイルをあてて、このように表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/228508/f468ff36-018f-69ff-d4fb-a3a1b463ac0a.png)

## 最後に
ライターにマークダウンで記事を書いてもらいつつも、目次を自動生成したりスタイルあてたりをflexmark-javaで実現しています。また必要になれば、拡張用のAPIがあるので、それを元に独自拡張を作る予定でいます。では、良い年末を！