PlantUML 埋め込み AsciiDoc の Gradle を用いた HTML 一括変換

· Read in about 6 min · (2852 words) ·

最近仕事で機能仕様書や設定手順書などの開発ドキュメントの AsciiDoc 化を推し進めています。 私個人は今まで Markdown や reStructuredText (reST) を用いた設計文書の記述をよくやっていたのですが、たまたま最近 (JJUG CCC 2017 Fall や社内のディスカッションなどで) AsciiDoc を推す声を多く聞いたこともありこっちもやってやるかと思うようになりました。

AsciiDoc

また、併せて PlantUML も導入されました。テキストで UML を表現できるため Git フレンドリーなのと、そこそこ綺麗にレンダリングでき、さらにレイアウトがツール任せなので個人差が出にくいのがかえってチーム開発では嬉しかったりします。

もちろん、複雑な UML を書くときは愛用 (?) の astah professional で書くこともありますが、最新の macOS での操作性が不具合対応のため劣化したのと、以前からマルチディスプレイ環境でフリーズすることが多く、最近はよほどのことがないかぎり起動しなくなりました。もうライセンスを更新することはないかなぁ (Change Vision さんごめんなさい・・・!)。

PlantUML

AsciiDoc はプロセッサとして Ruby 製の Asciidocor がポピュラーで、さらにこの拡張である asciidoctor-diagram を導入することで AsciiDoc 内に PlantUML レンダー結果を埋め込むことができます。 これは素晴らしいですね。しかし AsciiDoc/Asciidoctor 自体はディレクトリ内のファイルを一括で (しかも再帰的に) HTML 化することはできません。そこで便利なのが・・・

Gradle

Gradle です。

Asciidoctor には Gradle Plugin があり、名前はずばり Asciidoctor Gradle Plugin です。

Asciidoctor Gradle Plugin | Asciidoctor

https://asciidoctor.org/docs/asciidoctor-gradle-plugin/

Maven Plugin もあるようですね。Maven にこだわりたいというお方はここでお別れです😁

Asciidoctor Gradle Plugin は、Ruby 製の Asciidoctor を JRuby で動かしてしまうというものです。Gradle (Groovy の DSL) から JRuby が呼ばれて Ruby が動くのです。夢のような技術ですね✨ なお、JRuby を用いた Asciidoctor の Java バインディングを AsciidoctorJ と呼びます。

Asciidoctor 単体

それでは、まずは AsciiDoc 単体で動作確認してみましょう。

build.gradle を作ります。

apply plugin: 'org.asciidoctor.convert'
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.3'
    }
}

デフォルトの対象ディレクトリは src/docs/asciidoc です。それに習ってファイルを配置してみます。

$ tree
adoc-test
├── build.gradle
└── src
    └── docs
        └── asciidoc
            ├── func.adoc
            └── index.adoc

4 directories, 3 files

💡 ソースディレクトリを変更したい場合は、 build.gradleasciidoctor{ sourceDir = file('./docs') } のようにパスを教えてあげる必要があります。

adoc の中身はなんでも良いですが、今回はサンプルとしてこんなものを置いてみました (index.adoc の中身)。

= Asciidoc てすと
mikan <mikan@aosn.ws>
v0.1, 2018-02
ifdef::env-github,env-browser[:outfilesuffix: .adoc]
:toc:

== 仕様書

* link:func{outfilesuffix}[機能仕様書]

== 用語集

.一般用語
[format="csv",options="header"]
|===
用語,読み方,説明
Asciidoc,あすきーどっく,文書マークアップ言語のひとつ
PlantUML,ぷらんとゆーえむえる,テキストベースの UML 記述ツール
|===

💡 HTML 出力した際のリンクを .adoc ではなく .html とするために outfilesuffix 変数の切替えを利用しています。上記のようにすることで、GitHub や IDE 等におけるレンダリングは .adoc に、HTML 出力結果は .html となります [参考文献1]

準備ができたら、ビルドしてみましょう。

$ gradle asciidoctor

BUILD SUCCESSFUL と表示され、build/asciidoc/html5 に html ファイルができていれば成功です。早速開いてみましょう。

Asciidoctor 出力 HTML

ばっちりですね✨ 文書間のリンクもちゃんと .html になっていることが確認できるはずです。

💡 アウトプットディレクトリを変更したい場合は、 build.gradleasciidoctor{ sourceDir = file('./docs') } のようにパスを教えてあげる必要があります。

さて、これで仕事が終わるなら簡単すぎてブログ記事になりません。問題はここからなのです・・・!

PlantUML を埋め込んでレンダリングする

先に PlantUML をセットアップしておきます。今回レンダリングするために PlantUML と Graphviz のセットを導入しておく必要があります。Mac の Homebrew であれば brew install plantuml と叩くだけで準備は完了です。Windows は Chocolatey であれば両方パッケージが揃っていて手軽です。既に IDE 上でプレビューするためのプラグイン等の環境構築を行っていれば、おそらくそのまま Gradle からも呼び出せることでしょう (私のおすすめは IntelliJ の AsciiDoc プラグインです)。

さて、先ほどのサンプルでは index.adoc ともう一つ func.adoc というファイルを用意していました。そちらに PlantUML を書き込んでみます (中身にツッコミはなしでオナシャス)。

= 機能仕様書
mikan <mikan@aosn.ws>
v0.1, 2018-02
:toc:

== ユースケース図

[plantuml]
----
@startuml
:ユーザー: -> (本を借りる)
:ユーザー: -> (本を返却する)
@enduml
----

そして、これをビルドする build.gradle ですが… 最終的に使うのは冒頭で軽く触れたように asciidoctor の diagram 拡張になります。

Asciidoctor Diagram | Asciidoctor

https://asciidoctor.org/docs/asciidoctor-diagram/

ですが、これ自体はそもそも Ruby で require して使うものです。JRuby だとどうなる!? さらにそれを Gradle から指示するには!?

今回役にたったのは JRuby Gradle Plugin です。Asciidoctor Gradle Plugin との合わせ技が可能でした。

JRuby/Gradle

http://jruby-gradle.org/

・・・そして試行錯誤を重ねて私の手元で動くところまで書き加えた build.gradle はこんな感じになりました。

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.github.jruby-gradle:jruby-gradle-plugin:1.5.0'
        classpath 'org.asciidoctor:asciidoctor-gradle-plugin:1.5.3'
        classpath 'org.asciidoctor:asciidoctorj-diagram:1.5.4.1'
    }   
}

apply plugin: 'org.asciidoctor.convert'
apply plugin: 'com.github.jruby-gradle.base'

dependencies {
    gems 'rubygems:asciidoctor-diagram:1.5.7'
}

asciidoctor {
    requires = ['asciidoctor-diagram']
}

それでは gradle を叩いてみましょう!

$ gradle clean asciidoctor

Asciidoctor + PlantUML 出力 HTML

レンダリング結果が埋め込まれれば成功です!🎊

それにしても、Groovy に JRuby に Java に・・・すごい合わせ技でした (※ PlantUML は Java でできている)。

トラブルシューティング

喜びはつかの間、このソースを GitHub に置いて push 通知を Webhooks で飛ばし適当なサーバーで Gradle ビルドを実行、生成 HTML をその場でホスティングするという仕組みを社内で構築、展開したのですが、そのときにいくつかのトラブルに遭遇しました。手元で実行するまでの間にハマったものもふくめ、トラブルシューティングとして記録を残しておきたいと思います。

Java のバージョン

これまで紹介した build.gradle では Java 8 でしか動作せず、Java 7 環境や 9 環境が設定されている環境では動きませんでした。つらい!!まあ、JRuby というだけで嫌な予感がしてましたけどね・・・。複数バージョン入っている場合は JAVA_HOME をいじりながらやりくりする必要があります。

Asciidoctor Gradle Plugin のバージョン

asciidoctor-gradle-plugin は 1.5.3 をお使いください!これより新しくすると (1.5.7 等)、PlantUML レンダリングが最初の一発だけしか正しく行われないという現象に遭遇することがあります [参考文献2]

PDF 埋め込み機能

Asciidoctor には PDF を出力する機能を追加することができますが、今の所 PlantUML のレンダリング結果を拾ってくれません ('org.asciidoctor:asciidoctorj-pdf:1.5.0-alpha.16' で検証)。 ログを見ると png レンダリングされたファイルがある outputDir のパスではなく sourceDir のほうを読みに行ってしまっている様子。 あと一歩な気がする! そこで、 asciidoctor ブロックを以下のように設定し、正しいパスを教えてあげます。

asciidoctor {
    backends = ['html5','pdf']
    requires = ['asciidoctor-diagram']
    attributes "imagesdir": buildDir
}

buildscriptdependencies に次を追記するのもお忘れなく。

classpath 'org.asciidoctor:asciidoctorj-pdf:1.5.0-alpha.16'

2018/04/13 加筆しました (Thanks @osamus)

org.jruby.ext.openssl がないと言われる

やっつけですが、build.gradlebuildscriptdependencies に以下を追加することで回避できたことがあります:

classpath 'rubygems:jruby-openssl:0.9.21'

PlantUML レンダリング結果で日本語が豆腐になる

Java が日本語フォントを認識していないせいです。JRE の /jre/lib/fontsfallback というディレクトリを作り、そこにフォントのシンボリックリンクを突っ込むことで解決できます [参考文献3]

エンジニアフレンドリーなドキュメンテーション環境の追求は続く…

Stay tuned & Happy hacking!

参考文献

  1. Frequently Asked Questions (FAQs) and Troubleshooting - asciidoctor.org
  2. Extentions dissapear with gradle daemon after first run · Issue #222 · asciidoctor/asciidoctor-gradle-plugin
  3. Java 実行環境のフォント - ArchWiki
  4. asciidoctor/asciidoctorj-pdf: AsciidoctorJ PDF bundles the Asciidoctor PDF RubyGem (asciidoctor-pdf) so it can be loaded into the JVM using JRuby.