Edited at
D言語Day 8

D言語でドキュメント生成する方法のまとめ


概要

各種ツールの紹介と、インストール方法(ビルド方法)、使用方法、適用例を紹介します。



  • 標準の方法: 訓練されていなきゃ困難だけど、一番細かく作りこめる。


  • candydoc: そこそこ簡単だけど、dubでの設定がムズイ。


  • ddox: dubでサポートしている。改造例も紹介します。


  • adrdox: ビルドしないとだめで大変。dubにパッケージとして登録すると勝手にページが作られます。たぶん。


  • harbored-mod: dubで簡単にビルド・利用ができます。


  • doxygen: 特に特別な設定なく使えます(一部対応、とのことですが)。使ってる人見たことないです。


はじめに

はたらき方改革が叫ばれている昨今、我々エンジニアにとっても業務効率化は重要視されています。これに対して設計業務の一端を自動化するのは非常に有益です。ドキュメントの自動生成やCASEツールの活用、テスト自動化などなど。

この中でも、D言語は詳細設計をソースコード内で行い、ドキュメントを自動生成する方法を標準でサポートしています。

しかしながら、標準の方法ですべてを行うのは大変です。具体的には、標準の方法では装飾なしのHTMLが生成されるだけで、スタイルシートやサイト内検索などのあれやこれやは提供してくれません。

そこで、ドキュメント化のためのツールがいくつか開発されています。

各ツールは、HTMLのひな形や、CSS、Javascriptによる検索などを提供して、そこそこ見栄えよく、利便性を備えたドキュメントの生成を行ってくれます。

本記事では、各種ドキュメント生成の方法をまとめます。


標準の方法

dmdに標準で搭載されているドキュメント生成方法です。詳細な使用方法は公式サイトを参照してください。

すごく簡単な方法として、以下のようにコメントを記載すると簡単です。


とりあえず出力するだけ

///

void foo() { }

コメントの中身がなくとも、このようにすることで生成されるドキュメントに関数名や引数などは出力されるので、関数名だけで内容が十分理解できるようなものはこれだけでも十分に効果があります。たとえば、getFilenameとかいう関数名に「ファイル名を取得する」とかコメントをつけるのはあまりに自明すぎて馬鹿らしいですね。


概要だけのパターン

/// fooの概要

void foo() { }

/**********************************

* fooの概要
*/

void foo() { }

書かないよりましって思ったらこう書きます。


直前のと同じ場合

/// ditto

void foo() { }


ちょっと詳細に書く

/**********************************

* fooの概要
*
* fooの詳細説明
*/

void foo() { }

概要と詳細説明の間に1行あけるのがポイントです。


パラメータの説明とかしたい

/**********************************

* fooの概要
*
* fooの詳細説明
* Params:
* x = パラメータxの説明
* Returns:
* 戻り値の説明
*/

int foo(int x) { return x + 1; }

ここまで記載するとそこそこ立派なドキュメントになりますね。ここではParamsReturnsを使用していますが、いろいろなブロックがあります。適宜使用するといいと思います。ただ、このような書き方だと、doxygenでドキュメント生成するときにはうまくレイアウトされません。


こう書いてもいいです

//////////////////////////

/// fooの概要
///
/// fooの詳細説明
void foo() { }

/++

+ fooの概要
+
+ fooの詳細説明
+/

void foo() { }

/** */の形式で書くのが嫌な場合はこれらでもいいです。ただ、Phobosは/** */のスタイルですので、こう書いている人は多いような気がします。


標準の方法のインストール方法

dmdの付属機能を使用するため、不要です。あえて言えば、makeなどができると便利だと思います。


標準の方法の使用方法

以下のコマンドにより、src/main.dからdoc/main.htmlを生成することができます。

dmd main.d -D -o- -c src/main.d -Df doc/main.html

こうやって標準機能のドキュメント生成を行う場合は、Makefileを作成するケースが多いようですね。Phobosのドキュメントが個人的には見やすいように感じるので、あの体裁でもっと簡単に出力できるといいのですけど…。


標準の方法の適用例


CanDyDoc

標準の方法だけだとCSSなどの体裁が整っておらず、またモジュールの一覧なども自動で生成されない不便です。そこで、モジュール一覧と簡単なCSS、HTMLのテンプレを使って見栄えのいいものにしましょう、というのがCanDyDocです。CanDyDocはツールというより、標準の方法をいい感じに使う方法といった方が適切かもしません。かなり昔に作られたpublic domainなプロジェクトで、昔D言語のパッケージをtracで管理していた際の代物です。

非常にシンプルなドキュメントを生成することができます。


CanDyDocのインストール方法

ドキュメント生成先のフォルダにCanDyDocのファイルを配置します。

git clone https://github.com/eldar/candydoc.git


CanDyDocの使用方法

標準のドキュメント生成方法に加えて、モジュール一覧用のddocファイルと、テンプレ用のcandy.ddocを指定します。

モジュール一覧用のファイルは自分自身で以下のように定義します。

MODULES =

$(MODULE_FULL main)
$(MODULE_FULL test)

HTMLの生成は以下のようにdmdにより行います。

dmd -D -o- -c src/main.d doc/candydoc/modules.ddoc doc/candydoc/candy.ddoc -Dfdoc/main.html

dmd -D -o- -c src/test.d doc/candydoc/modules.ddoc doc/candydoc/candy.ddoc -Dfdoc/test.html

もちろん、一つずつHTMLを生成するのは大変なので、makeなどを使用するといいでしょう。また、modules.ddocもシェル芸とか使ってファイル一覧から機械的に生成するようにするといいでしょう。

dubでCanDyDocの生成を行うのは結構難しいです。postBuildCommandsなどでmake走らせるとか言う強引な方法が一番楽そうです。


candydocの適用例


ddox

D言語のパッケージツールdubでデフォルト対応していて、vibe.dで使われているドキュメント生成ツールです。


ddoxのインストール方法

dubが利用できる状態であれば、特にインストールを必要とせずに使用することができます。(ただし、その場合細かな設定はできません)

自分でビルドする方法もあります。dubでそこそこ簡単にビルドできます。

ただし、Windows(dmd)だとライブラリのOMFがいい感じにできていないせいか、うまくいかないことが多く、LDCや64bit版や、mscoff形式でビルドするとうまいこと行くことが多いです(というか、そのせいでdubから直接使用する方法が使えないことがあります)。

dub build -a=x86_64 -b=release -c=application --compiler=ldc2


ddoxの使用方法


dubから使う方法

dubを使って、以下のようにビルドすることで、実行ファイルやライブラリではなく、ドキュメントを生成することができます。

dub build --build=ddox


ddoxを直接使う方法

まず、dmdのJSON生成機能を用いてddocs.jsonを生成します。

dmd -Xfdocs.json -o- -c src/a.d src/b.d src/c.d src/e.d

ddox filterにより、ドキュメント生成に使用する定義を抽出して、ddox generate-htmlによってHTMLを生成します。

ddox filter docs.json --min-protection Protected

ddox generate-html docs.json doc --navigation-type=DeclarationTree

上記のように、多少レイアウトを弄ることができます。(上記では--navigation-type=DeclarationTreeでナビゲーションバーの表示を切り替えています)


カスタマイズ

ddoxの出力するHTMLはdietテンプレートを用いて作成されており、これを編集することで細かくカスタマイズすることが可能です。

さらに、カスタマイズしたddoxをdubパッケージで公開することで、dubから直接使用することができます。

以下のリストは実際にdubパッケージに登録されているddoxをカスタマイズしたテーマで、カスタマイズされたドキュメントを生成するためのツールです。

これらはdubパッケージで公開済みなので、以下のようにすることでカスタムテーマに対応することができます。(この例はscodを使用する例です)


dub.json

{

:
:
"-ddoxTool": "scod"
:
:
}

ドキュメント生成はdubを使った方法と同じです。

dub build --build=ddox

これ、公開必須とかいささか気軽さに欠けると思います。もっと気軽にできればいいのに。"-ddoxTool": {"path": "custom_ddox"}ってな感じに。


自分でカスタマイズしたddoxテーマをローカルで使用するdub設定

パッケージで公開するのもちょっと…っていうような場合は、以下のようにすることで、自前改造のddoxを使ってドキュメント生成することができます。

例えばこんな感じに以下の3つのファイルをいじくって…

ddox.inc.composite.dt


ddox.inc.composite.dt

- import ddox.api;

- import ddox.highlight;
- import std.algorithm;
- import std.string : toLower;
- import std.typetuple;

- void outputCompositePrototype(CompositeTypeDeclaration item)
- auto cls_item = cast(ClassDeclaration)item;
- auto intf_item = cast(InterfaceDeclaration)item;

| <span class="kwd">#{item.kind.to!string.toLower()}</span> <span class="typ">#{item.name}</span>
- info.renderTemplateArgs(&_diet_output, item);

- bool first = true;
- void outputBase(CachedType tp)
- if (tp.typeName.among("Object", "object.Object"))
- return;
| !{first ? "<br>&nbsp;&nbsp;: " : "<br>&nbsp;&nbsp;, "}!{info.formatType(tp, false)}
- first = false;

- if (cls_item && (cls_item.baseClass || cls_item.derivedInterfaces.length))
- if (cls_item.baseClass)
- outputBase(cls_item.baseClass);
- foreach (intf; cls_item.derivedInterfaces)
- outputBase(intf);

- if (intf_item && intf_item.derivedInterfaces.length)
- foreach (intf; intf_item.derivedInterfaces)
- outputBase(intf);

- if (item.templateConstraint.length)
| #[br]&nbsp;&nbsp;
- if (item.templateConstraint.length)
br
| <span class="kwd">if</span> <span class="pun">(</span>!{highlightDCode(item.templateConstraint)}<span class="pun">);</span>
- else
span.pun ;

- void outputCompositeMembers(CompositeTypeDeclaration item, int hlevel)
- alias TypeTuple!(InterfaceDeclaration, ClassDeclaration, StructDeclaration, UnionDeclaration, EnumDeclaration, AliasDeclaration, TemplateDeclaration) kinds;
- static const kindnames = ["Inner interfaces", "Inner classes", "Inner structs", "Unions", "Enums", "Aliases", "Templates"];
- static const kindnames_sing = ["Inner interface", "Inner class", "Inner struct", "Union", "Enum", "Alias", "Template"];

- FunctionDeclaration[] properties, methods, constructors;
- foreach( itm; getDocGroups!FunctionDeclaration(item) )
- if( itm.hasAttribute("@property") )
- properties ~= itm;
- else if( itm.name != "this" )
- methods ~= itm;
- else
- constructors ~= itm;

- if( constructors.length )
section
- heading(hlevel, "Constructors");
table
col.caption
tr
th Name
th Description
- foreach( p; constructors )
tr
td
a(href="#{info.linkTo(p)}", class=declStyleClasses(p))
code= p.name
span.tableEntryAnnotation (#{p.parameters.map!(param => param.name[]).joiner(", ")})
td!= info.formatDoc(p.docGroup, 3, sec => sec == "$Short")

- if( hasChild!VariableDeclaration(item) )
section
- heading(hlevel, "Fields");
table
col.caption
tr
th Name
th Type
th Description
- foreach( f; getChildren!VariableDeclaration(item) )
tr
td
a(href="#{info.linkTo(f)}", class=declStyleClasses(f)) <code>#{f.name}</code>
td.typecol!= info.formatType(f.type)
td!= info.formatDoc(f.docGroup, 3, sec => sec == "$Short")

- if( properties.length )
section
- heading(hlevel, "Properties");
table
col.caption
tr
th Name
th Type
th Description
- foreach( p; properties )
- auto mems = p.docGroup ? p.docGroup.members : [cast(Entity)p];
tr
td
a(href="#{info.linkTo(p)}", class=declStyleClasses(p)) <code>#{p.name}</code>
span.tableEntryAnnotation= anyPropertyGetter(mems) ? anyPropertySetter(mems) ? "[get, set]" : "[get]" : "[set]"
td.typecol!= info.formatType(getPropertyType(mems))
td!= info.formatDoc(p.docGroup, 3, sec => sec == "$Short")

- if( methods.length )
section
- heading(hlevel, "Methods");
table
col.caption
tr
th Name
th Description
- foreach( p; methods )
tr
td
a(href="#{info.linkTo(p)}", class=declStyleClasses(p))
code= p.name
span.tableEntryAnnotation (#{p.parameters.map!(param => param.name[]).joiner(", ")})
td!= info.formatDoc(p.docGroup, 3, sec => sec == "$Short")
tr
td(colspan=2)
include ddox.inc.function
- auto fdecl = cast(FunctionDeclaration)p;
- assert(fdecl !is null, "Invalid node of function kind: " ~ p.qualifiedName.to!string);
- outputFunctionPrototypeList(fdecl);

- foreach( i, kind; kinds )
- if( hasChild!kind(item) )
section
- heading(hlevel, kindnames[i]);
table
col.caption
tr
th Name
th Description
- foreach( grp; getDocGroups!kind(item) )
tr
td
a(href="#{info.linkTo(grp)}", class=declStyleClasses(grp))
code= grp.name
td!= info.formatDoc(grp.docGroup, 3, sec => sec == "$Short")



ddox.inc.function.dt


ddox.inc.function.dt

- import ddox.api;

- import ddox.highlight;
- import std.algorithm;

- void outputFunctionPrototype(FunctionDeclaration item)
- auto docgroup = item.docGroup;
- auto mems = docgroup.members;
- foreach (size_t pi, pd; mems)
- auto proto = cast(FunctionDeclaration)pd;
- if (!proto) continue;
- auto attribs = proto.attributes.dup;
- if (pi > 0)
br
br
- auto rettype = proto.name == "this" ? "" : info.formatType(proto.returnType, false) ~ " ";
- if (rettype.length == 1 || rettype == "{null} ")
- foreach (i, attr; attribs)
- if (attr == "auto")
- rettype = highlightDCode(attr) ~ " ";
- break;
- attribs = attribs.remove!(a => a.str == "auto");
- auto attribute_prefix = highlightDCode(getAttributeString(attribs, AttributeStringKind.functionPrefix));
- auto attribute_suffix = highlightDCode(getAttributeString(attribs, AttributeStringKind.functionSuffix));
- auto variadic_suffix = highlightDCode(getVariadicSuffix(proto.type));
- if (!proto.templateConstraint.length) attribute_suffix ~= "<span class=\"pun\">;</span>";

- if (proto.parameters.length)
| !{attribute_prefix}!{rettype}#[span.pln= proto.name]
- info.renderTemplateArgs(&_diet_output, proto);
span.pun (
br
- foreach (size_t i, p; proto.parameters)
- auto pattribs = highlightDCode(getAttributeString(p.attributes, AttributeStringKind.normal));
- auto suffix = i+1 < proto.parameters.length ? "<span class=\"pun\">,</span>" : variadic_suffix;
- if (p.initializer)
| &nbsp;&nbsp;!{pattribs}!{info.formatType(p.type, false)} #[span.pln= p.name] #[span.pun =] !{highlightDCode(p.initializer.valueString)}!{suffix}
- else
| &nbsp;&nbsp;!{pattribs}!{info.formatType(p.type, false)} #[span.pln= p.name]!{suffix}
br
|#[span.pun )]!{attribute_suffix}
- else
| !{attribute_prefix}!{rettype}#[span.pln= proto.name]
- info.renderTemplateArgs(&_diet_output, proto);
| #[span.pun<> (]!{variadic_suffix}#[span.pun )]!{attribute_suffix}

- if (proto.templateConstraint.length)
br
|#[span.kwd if] #[span.pun (]!{highlightDCode(proto.templateConstraint)}#[span.pun );]

- void outputFunctionPrototypeList(FunctionDeclaration item)
ul
- auto docgroup = item.docGroup;
- auto mems = docgroup.members;
- foreach (size_t pi, pd; mems)
li
- auto proto = cast(FunctionDeclaration)pd;
- if (!proto) continue;
- auto attribs = proto.attributes.dup;
- auto rettype = proto.name == "this" ? "" : info.formatType(proto.returnType, false) ~ " ";
- if (rettype.length == 1 || rettype == "{null} ")
- foreach (i, attr; attribs)
- if (attr == "auto")
- rettype = highlightDCode(attr) ~ " ";
- break;
- attribs = attribs.remove!(a => a.str == "auto");
- auto attribute_prefix = highlightDCode(getAttributeString(attribs, AttributeStringKind.functionPrefix));
- auto attribute_suffix = highlightDCode(getAttributeString(attribs, AttributeStringKind.functionSuffix));
- auto variadic_suffix = highlightDCode(getVariadicSuffix(proto.type));
- if (!proto.templateConstraint.length) attribute_suffix ~= "<span class=\"pun\">;</span>";

- if (proto.parameters.length)
| !{attribute_prefix}!{rettype}#[span.pln= proto.name]
- info.renderTemplateArgs(&_diet_output, proto);
span.pun (
- foreach (size_t i, p; proto.parameters)
- auto pattribs = highlightDCode(getAttributeString(p.attributes, AttributeStringKind.normal));
- auto suffix = i+1 < proto.parameters.length ? "<span class=\"pun\">,</span>" : variadic_suffix;
- if (p.initializer)
| &nbsp;&nbsp;!{pattribs}!{info.formatType(p.type, false)} #[span.pln= p.name] #[span.pun =] !{highlightDCode(p.initializer.valueString)}!{suffix}
- else
| &nbsp;&nbsp;!{pattribs}!{info.formatType(p.type, false)} #[span.pln= p.name]!{suffix}
|#[span.pun )]!{attribute_suffix}
- else
| !{attribute_prefix}!{rettype}#[span.pln= proto.name]
- info.renderTemplateArgs(&_diet_output, proto);
| #[span.pun<> (]!{variadic_suffix}#[span.pun )]!{attribute_suffix}

- if (proto.templateConstraint.length)
|#[span.kwd if] #[span.pun (]!{highlightDCode(proto.templateConstraint)}#[span.pun );]



ddox.module.dt


ddox.module.dt

extends ddox.layout

block ddox.defs
- import ddox.api;
- import std.algorithm : canFind, map, joiner;
- import std.typetuple;

block ddox.title
- title = "Module " ~ info.mod.qualifiedName.to!string;

block ddox.description
p!= info.formatDoc(info.mod.docGroup, 2, sec => sec == "$Short")

|!= info.formatDoc(info.mod.docGroup, 2, sec => sec == "$Long")

block ddox.sections
section!= info.formatDoc(info.mod.docGroup, 2, sec => !canFind(["License", "Copyright", "Authors", "$Short", "$Long", "Source"], sec))

block ddox.members
include ddox.inc.function
- alias TypeTuple!(FunctionDeclaration, InterfaceDeclaration, ClassDeclaration, StructDeclaration, UnionDeclaration, EnumDeclaration, TemplateDeclaration) kinds;
- static const kindnames = ["Functions", "Interfaces", "Classes", "Structs", "Unions", "Enums", "Templates"];
- static const kindnames_sing = ["Function", "Interface", "Class", "Struct", "Union", "Enum", "Template"];

- foreach( i, kind; kinds )
- if( hasChild!kind(info.mod) )
section
h2= kindnames[i]
table
col.caption
tr
th Name
th Description
- foreach( grp; getDocGroups!kind(info.mod) )
tr
td
code
a(id=grp.name[], class=declStyleClasses(grp), href=info.linkTo(grp))= grp.name
- if (auto fd = cast(FunctionDeclaration)grp)
span.tableEntryAnnotation (#{fd.parameters.map!(p => p.name[]).joiner(", ")})
td!= info.formatDoc(grp.docGroup, 3, sec => sec == "$Short")
- if (kindnames[i] == "Functions")
tr
td(colspan=2)
- auto fdecl = cast(FunctionDeclaration)grp;
- assert(fdecl !is null, "Invalid node of function kind: " ~ grp.qualifiedName.to!string);
- outputFunctionPrototypeList(fdecl);

- alias TypeTuple!(EnumMemberDeclaration, VariableDeclaration, AliasDeclaration) tkinds;
- static const tkindnames = ["Manifest constants", "Global variables", "Aliases"];
- static const tkindnames_sing = ["Manifest constant", "Variable", "Alias"];

- foreach( i, kind; tkinds )
- if( hasChild!kind(info.mod) )
section
h2 #{tkindnames[i]}
table
col.caption
tr
th Name
th Type
th Description
- foreach( f; getDocGroups!kind(info.mod) )
tr
td
a(id=f.name[], class=declStyleClasses(f), href=info.linkTo(f))
code= f.name
td
- if( f.type )
|!= info.formatType(f.type)
td!= info.formatDoc(f.docGroup, 3, sec => sec == "$Short")

block ddox.authors
|!= info.formatDoc(info.mod.docGroup, 0, sec => sec == "Authors")
block ddox.license
|!= info.formatDoc(info.mod.docGroup, 0, sec => sec == "License")
block ddox.copyright
|!= info.formatDoc(info.mod.docGroup, 0, sec => sec == "Copyright")



dub build -a=x86_64 -b=release -c=applicataion

ってな感じでビルドして…

ドキュメントを生成したいプロジェクトのdub.jsonにはサブパッケージを定義して…

dub.json


dub.json

{

:
:
"subPackages": [
"doc"
],
:
:
}


docフォルダの中にこんな感じにサブパッケージを作って…

doc/dub.json


doc/dub.json

{

"name": "doc",
"description": "document.",
"copyright": "Copyright © 2018, SHOO",
"targetType": "library",
"versions": ["Test"],
"buildOptions": ["syntaxOnly"],
"importPaths": [
".."
],
"sourcePath": "../src",
"dflags": ["-Xfdocs.json"],
"dependencies": {
"ddox": "~>0.16.0"
},
"postBuildCommands-windows": [
"robocopy $DDOX_PACKAGE_DIR\\public\\ $PACKAGE_DIR\\ /E & time /T>nul",
"ddox filter docs.json --min-protection Protected",
"ddox generate-html docs.json doc --navigation-type=DeclarationTree",
"if exist __dummy.html del __dummy.html"
]
}


こうじゃ

dub build :doc


ddoxの適用例


関連記事


adrdox

最近、dubのパッケージに登録されているものについては、自動的にドキュメントが生成されるようになっています。その生成ツールがこのadrdoxです。


Adam D. Ruppeさんが作成しているドキュメント生成ツールで、バイナリを配布している公式ページはないもよう。


adrdoxのインストール

インストールというか、ビルド方法は以下の通り。普通にdubでビルドできます。

git clone https://github.com/adamdruppe/adrdox.git .

dub build -a=x86_64 -b=release

build以下に作成されるファイルが必要なファイルです。


  • adrdox.exe

  • script.js

  • skeleton-default.html

  • style.css

  • search-docs.html (たぶんcopyFilesに指定し忘れている)

  • search-docs.js (たぶんcopyFilesに指定し忘れている)


adrdoxの使用方法


  1. ビルドにより生成された各ファイルを、ドキュメントを生成するフォルダにいれます。

  2. 以下のコマンドでドキュメントを生成

./adrdox -i ../src


adrdoxの適用例


harbored-mod

adrdox同様、DDocとMarkdownを使ったドキュメント化コメントを取り扱ってHTMLを吐き出すツールです。

dubパッケージとして登録されているアプリケーションなので、dubで比較的簡単に取り扱うことができます。


インストール方法

dubに登録されているアプリケーションですので、インストールは不要です。

dub fetch harbored-mod

dub run harbored-mod -- src

もちろん、ビルドしてもいいですね。binフォルダが作られて、その中にhmodというバイナリが生成されます。

(以下はldc2でx86_64向けにリリースビルドしてみる例です)

git clone https://github.com/dlang-community/harbored-mod.git

cd harbored-mod
dub build -a=x86_64 -b=release --compiler=ldc2


使用方法

基本的には以下でOKです。以下のようにすると、srcフォルダ以下の.dファイルが検索され、ドキュメントを生成してくれます。


srcフォルダ以下のソースをドキュメント化

dub run harbored-mod -- src



ヘルプ

dub run harbored-mod -- -h



設定ファイル生成(hmod.cfg)

dub run harbored-mod -- -g



適用例


doxygen

この有名なドキュメント生成ツールも、D言語をサポートしています。

たぶんJavadoc方式でコードにコメントを付けることでHTMLを生成してくれると思います。


doxygenのインストール方法

公式ページ参照。(あまり深く説明する気がない)

場合によってはGraphvizもインストールするといいでしょう。

Qiita解説記事


doxygenの使用方法

まず、以下のようなコマンドでDoxyfileを作成します。

doxygen -g

doxyfileを編集します。D言語専用の設定は特にありません。

ドキュメント生成は以下。

doxygen


適用例


  • (すみません、D言語での例は見たことありません)


まとめ

各種ツール(標準, candydoc, ddox, adrdox, harbored-mod, doxygen)の概要、インストール方法、使用方法、適用例についてご紹介いたしました。

現時点におけるD言語の「最強のドキュメント生成ツール!これで万事解決!!!」みたいな決定打は今のところない感じです。

最近はadrdoxみたいな選択肢も増えているので、今後に期待ですね。