AsciiDocエディタを作りました
できたものはこちら
AsciidocEditor
ライブプレビューはもちろん、asciidoctorとace editorの一部の設定をGUIからいじれるようになっているのと、asciidoctorのデフォルトCSSとソースハイライターCodeRayのCSSをローカルに持ってくるようになっているので、CSSに強い方は好きなデザインでかけると思います。
動機
普段は大学生をしています。プログラミングを始めたのは大学に入学してからで、それまではろくにパソコンも触ってきませんでした。入学してから実習でやっていることと言えば、アルゴリズムを学んで、C言語で書くくらいです。アルゴリズムを学ぶのも実装してみるのも、大事で面白いことだと思います。しかし、それだけでは物足りないなぁと感じるようになってきていました。そこでGUIアプリケーションを作ろうと思い立ちました。
完成までの流れ
前期(学習)
※作り始める前のスキル
- C言語(教科書のアルゴリズム実装)
- Python(OpenCVを使った画像処理)
C言語は慣れてきましたがPythonは雛形が用意されてたので大して学んでません。
Javaに入門
教員の方が「少なくとも手続き型とオブジェクト指向型一つずつは触ってみたほうがいい。」みたいなことをおっしゃっていたので、あまりプログラミング言語に詳しくない僕はオブジェクト指向といったらJava!!という謎の考えでJavaに入門することに決めました。
いろんなところでオススメされていたので買いました。評判通り読みやすくまとまっていて、オブジェクト指向もなんとなくイメージできました。
下調べ
一通り上記の入門書を読んで入門した気になったのでJavaのGUIライブラリを調べました。
この段階で分かったことは以下の通りです。
- JavaのGUIはSwingかJavaFX
- JavaFXはJDKに同梱されなくなったこと
- JDKのリリースモデルが変わったこと
- プログラマを助けてくれるビルドツールなるものがあること
中期(中だるみ)
下調べをした結果、自分がなかなか面倒な言語選択をしてしまったと思い萎えました。JDKにJavaFXが同梱されなくなるということで情報収集が大変になると思ったからです。
後期(実装)
せっかく入門したのでやっぱり作ろうと思い直します。
何を作るか
下調べの際にmarkdownエディタを簡単に実装している方がいらっしゃったので、似たようなものにオリジナルの要素をつけようと考えました。色々調べるうちにAsciiDocなるものの存在を知ります。じゃあAsciiDocエディタを作ろう!
そして僕の考えた最強のAsciiDocエディタの機能がこちら
- エディタの基本機能(読み込み、保存)
- ライブプレビュー
- HTMLファイルに出力
これ作ったらオンリーワンだ!!
...そんなわけありませんでした。
AsciidocFX
この他にも素晴らしいものがあるとは思いますが、上記の機能に、AsciidocFXにはない機能をつけることを目標にしました。AsciidocFXはエディタをフレームいっぱいにすることができても、プレビュー画面をフレームいっぱいにすることはできません。これによって toc: left
と目次の位置を指定してもプレビューでは確認できないことがわかりました。(ほんとはできるのかもしれません...)
よって最終的な仕様は以下の通りになりました。
- エディタの基本機能(読み込み、保存)
- ライブプレビュー
- HTMLファイルに出力
- 全画面プレビュー
少し引っかかって開発が滞った部分を紹介します。
エディタの基本機能
JavaFXにおいてファイルを読み込んだり保存したりするのはFileChooser
クラスを使用します。
FileChooser fileChooser = new FileChooser();
File file = fileChooser.showOpenDialog(null);
この二行目の引数に渡すnullが問題で、アプリを全画面表示すると応答しなくなり、強制終了せざるを得なくなりました。代わりに引数にはメインのStage
を渡します。よくあるApplicationクラスを継承したサンプルは
public class Sample1 extends Application {
public static void main(String[] args) {
launch(args);
}
public void start(Stage primaryStage) throws Exception {
FXMLLoader fxmlLoader = new FXMLLoader();
Parent root = fxmlLoader.load(getClass().getResourceAsStream("/sample1.fxml"));
primaryStage.setTitle("sample 1");
primaryStage.setScene(new Scene(root, 900, 600));
primaryStage.show();
}
}
ですが、これを
public class Sample2 extends Application {
static Stage stage;
public static void main(String[] args) {
launch(args);
}
public void start(Stage primaryStage) throws Exception {
FXMLLoader fxmlLoader = new FXMLLoader();
stage = primaryStage;
Parent root = fxmlLoader.load(getClass().getResourceAsStream("/sample2.fxml"));
stage.setTitle("sample 2");
stage.setScene(new Scene(root, 900, 600));
stage.show();
}
}
としてFileChooser
に渡します
FileChooser fileChooser = new FileChooser();
File file = fileChooser.showOpenDialog(Sample2.stage);
こうするとメインのウィンドウを動かしてもFileChooser
のウィンドウが付いてきてくれます。
Dialog
クラスの場合は
Dialog<ButtonType> dialog = new Dialog<>();
dialog.initOwner(Sample2.stage);
で同じことができます。
ライブプレビュー(AsciiDoc -> HTML)
使用したライブラリ
- asciidoctorj
- asciidoctorj-diagram
- jsoup
asciidoctorjはRubyライブラリasciidoctorをJavaで使えるようにしてくれたものです。ここから生成される文字列をJavaFXのWebView
に流し込みます。
asciidoctorj-diagramを使っている例があまり見当たらなかったので参考までに書いておきます。
Asciidoctor asciidoctor = org.asciidoctor.Asciidoctor.Factory.create();
でインスタンスを生成したのち
asciidoctor.requireLibrary("asciidoctor-diagram");
とするとplantumlなどの機能が使えるようになります。
発生した問題1:ローカル画像が表示されない
JavaFXのWebView
にはWebEngine
のload
メソッドまたはloadContent
メソッドで読み込んで表示できます。今回はURLではなく、生のHTMLをString
として読み込むのでloadContent
メソッドを使用します。しかしこのメソッドでHTMLを読み込むと、src属性のパスにあるローカル画像を表示してくれません。必要だったのはファイルプロトコルfile://
を頭につけることでした。この問題を解決するために使ったのがjsoupです。
ArrayList<Element> src = document.getElementsByAttribute("src");
for (Element e : src) {
String attributeValue = e.attr("src");
if (!attributeValue.contains("data:") &&
!attributeValue.contains("https:") &&
!attributeValue.contains("file:")
) {
attributeValue = Paths.get(attributeValue).toUri().toString();
e.attr("src", attributeValue);
}
}
toUri
メソッドでファイルプロトコルが付きます。
発生した問題2:リンク関連の問題
JavaFXのWebView
に表示されているリンクをクリックすると、そのWebView
で読み込んでしまうので、もともと見ていたAsciiDocのプレビューが一時的にではありますが見れなくなってしまいました。これを解決するためにデフォルトのブラウザで開くようにしました。
//viewerはプレビュー用WebViewのfxmlコンポーネント
viewer.getEngine().getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == Worker.State.SCHEDULED) {
String urlLocation = viewer.getEngine().getLocation();
if (urlLocation.contains("http:") || urlLocation.contains("https:")) {
viewer.getEngine().getLoadWorker().cancel();
try {
Desktop.getDesktop().browse(new URI(urlLocation));
} catch (URISyntaxException | IOException e) {
e.printStackTrace();
}
}
}
});
もう一つの問題はaタグのページ内ジャンプができないことでした。今回は
viewer.getEngine().setJavaScriptEnabled(true);
とすることでJavaScriptを実行可能にし、jsoupでscriptタグを追加しアンカーリンク用のスクリプトを読み込ませました。
String anchorLinkScript =
"function anchorlink(id_name) {" +
"obj = document.getElementById(id_name);" +
"obj.scrollIntoView(true);" +
"}";
Element body = document.body();
body.appendElement("script").append(anchorLinkScript);
ArrayList<Element> elements = document.getElementsByTag("a");
for (Element e : elements) {
if (!(
e.attr("href").contains("http:") ||
e.attr("href").contains("https:")
)) {
String idName = e.attr("href").substring(1);
e.attr("onclick", "anchorlink(\'" + idName + "\')");
}
}
※AceとMacのJapaneseIM(一つ前の記事ですでに紹介)
JavaFXにAceを埋め込んだらMacのJapaneseIMがバグったので解消した
そんなこんなである程度機能が実装できました。
まだできていないこと
機能面
今回は文字を打つたびに変換しているので、asciidoctor-diagramの機能を使う場合は文字を打つたびに.pngファイルが生成されてしまいます。しかも、それを防ぐためにファイル名をつけようと
[plantuml, "sample"]
----
Bob->Alice: Hello
----
と上から順番に入力していくとsample.png自体は入力するたびにしっかり変わってくれるんですが、WebViewは一番最初に生成されたsample.pngしか表示してくれません(アプリを再起動すれば表示されるし、ExportしたHTMLも問題なく読み込んでくれる)。これに関しては全く分からなくて悩んでいます。
あとはPDF出力も盛り込みたかった機能の一つです。
配布面
基本的に僕はアプリといったらアイコンをダブルクリックしたら起動できるものだというイメージが強かったので、今回もそれを目指しました。具体的にはjlinkやinstall4j、badass-runtime-pluginを駆使して、クロスプラットフォームなアプリを配布したかったです。しかし、このまま出さないといつまでたっても公の場に成果物を出せない気がしたので一旦諦めてリリースしました。Java 13からはjpackageがあるかもしれないのでそれに期待です!
振り返って
一人で作るのはしんどい
誰にも言わずに一人でこれを作っていましたが、結局GitHubにリポジトリを作ってから4ヶ月もかかってリリースしました。リポジトリを作ったのはAsciiDocエディタを作ろうと思ってからなので、Javaに入門してからは8ヶ月くらいでしょうか。
理由として考えられるのは
- 知識の少なさ
- モチベーションの維持
です。これを作るまではコーディングに時間がかかるものだとばかり思っていましたが、分からないことを検索して飲み込む時間の方が数倍長かったです。また、コーディングがJavaの慣習的にどうだとか、スパゲッティになってるだとか、自分では気づけなさそうなことを教えてくれる人がいなかったのは痛かったです。
当たり前ですがモチベーションの維持はとても大切です。今回は中だるみの時期があったり、思うようには行きませんでしたが、割と小さなことでもモチベーションが上がることが分かりました。AsciidocFXにはない小さな機能を考えたり、初めてのGUIだったので一つ一つ想定通りに動くようになるたびに嬉しくなりました。
公式リファレンスを読むのが大事
なんとなくいつもプログラミング関連の検索をするときは、Qiitaに最初にくる癖がついていました。Qiitaは手早く知りたい情報を得られるので、楽でいいですよね。しかし、一から何か作ろうと思うと公式のドキュメントを一番最初に見るのが大事なのかもしれません。JavaFX 11からはJDKに同梱されなくなったので、そのためのドキュメントGetting Started with JavaFX 11なんてものがあることは最初のうちは知りませんでした。すごく分かりやすくまとまってました(小並感)。もう少し早く公式のリファレンスの偉大さに気づいていたら、開発期間を短くできたかもしれません。
それに気づいてからは、なるだけJavaも含め使用するライブラリのAPIドキュメントを覗くように心がけました。公式のAPIドキュメントだけでFirefoxのタブが何十個も開いていたときは、なんだかプログラミングしてる!!という気持ちになれて興奮しました。これもモチベーションの維持に一役買ったかと思います。
まとめ
初めて触る言語で初めてのGUIに挑戦しました。一人ではしんどいと書きましたが、達成感は結構あります(一人じゃなかったら達成感を分かち合えると思うので、どっちがどうとかは分かりません)。
これを読んでくださった僕みたいなプログラミング始めたばかりの方は、プログラミング言語に入門する前にしっかり下調べをすることをお勧めします(当たり前かもしれませんが...)。実装し始めてからは、調べながらコーディングをしていくことになると思いますが、入門書を読んで例題を書いてみるより遥かに技術が身についていくことがわかるので、きっと楽しくなるはずです。英語に対する抵抗をなくして、公式ドキュメントやStackoverflowなどを読んでみると捗ります(Stackoverflowには時々ガチガチのガチみたいな強い人がいらっしゃるので)。
そしてたまたま最後まで見てくださった先輩プログラマの方には、参考になるようなことは書いてなかったかもしれません。優しい方はGitHubをほんのちょっと覗いて、「このソースコードはアカン!!」とか叱咤激励してくださるとありがたいです。また、こんな言語、ライブラリ面白いよ!とか、開発始める前にこれ読んどけばよかったのに...みたいな記事を教えていただければと思います(乞食)。
読み返してみると、「何番煎じやねん」みたいな記事なってますが、最後まで読んでいただきありがとうございました。