【C#】今更ScriptCsを触ってみる

前書き

最近新しくお仕事用のPCを貰ったはいいんですが、こう、端的に言って、とんでもないクソスペックです。

Windows 7は別にいいんですが、32bitだし、メモリ1GBしかないし…。今どき1GBのメモリなんてExcel開いてるだけで枯渇しますよ。

まぁ開発用ではないので何だかんだで使えはするんですが、こんなクソスペックでちょっとIDEでも入れるかーなんてとても思えません。

でもちょっとしたI/O操作とかスクリプト組みたいよねーって気持ちは常にあるので、適当なスクリプト言語の環境を整えることにしました。

でもI/O操作を考えるとC#が一番楽なんだけどなぁ…Visual Studioをこいつで起動するのはなぁ…と悩んでいたところ、ScriptCsの存在を思い出したので、導入することにしました。

ScriptCsってなに?

Roslynちゃんの力をフルに使ってC#をそのままスクリプト言語のように扱えるようにしてしまった謎の実行環境です。

InfoQの記事がとてもわかりやすいので読んでおきましょう。

インストール方法

Download Zip Fileとか書いてあるんでそこを押したくなるんですが、これは罠です。GitHubのmasterブランチがドカンと落ちてきます。

インストールするにはChocolatey経由で落としてこいとのことです。雑ですね。

もう既にChocolateyをインストールしている人はこれで落としてきましょう。Chocolatey自体のインストール方法はこっそりと以前の記事に書いてあります。

ちなみに、Roslynを動かす関係上.NET Framework 4.5以上が必須です。

注意点

微妙にハマったところだけ先に紹介しておきます。

  • ScriptCSのカレントディレクトリはscriptcs.exeが置いてあるディレクトリではなく、scriptcs.exeを呼び出したディレクトリとなる。
    • I/O操作は勿論、#load(後述)で呼び出すスクリプトのパスにも関わってくるので注意が必要。
  • dynamicは使用できない。
    • そもそもRoslynが対応してないっぽい。
    • そのせいでDynamicObjectも使えない。折角のスクリプト言語っぽい言語仕様なのに…!

REPL

インストールし終わったらscriptcs.exeを引数なしで叩いてみましょう。REPLが起動するはずです。

C:\> scriptcs
scriptcs (ctrl-c or blank to exit)

> var message = "Hello, world!";
> Console.WriteLine(message);
Hello, world!
> 

usingもnamespaceもclassもMainメソッドも必要ありません。これだけで動きます。REPLでなく、ファイルに書きだしたものを読み込ませても当然動きます。Roslyn様々ですね。

スクリプトファイルの読み込み

引数にファイル名を渡してあげれば勝手に実行してくれます。拡張子はcsxが推奨されています。別にcsxじゃなきゃ読み込んでくれないわけでもないです。

ただまぁ、文字コードはBOMつきUTF-8にしておいてあげましょう。Roslynちゃんは渡された文字列を問答無用でBOMつきUTF-8としてパースします。

試しに先ほどのサンプルをtest.csxとして保存し、「scriptcs test.csx」と実行すればコンソール上に「Hello, world!」が表示されるはずです。

スクリプト内でのファイル読み込み

とりあえずこんなコードを用意しました。ファイル名は「Hoge.csx」です。

public class Hoge {
    public string Fuga {get; set;}
    public int Piyo {get; set;}
    
    public string Foo() {
        return "Foo";
    }
}

#loadを使うことでスクリプト内でこれを読み込むことが出来ます。ちなみにREPLだと簡単に変数の中身が確認できるので、printfデバッグがしやすいです…と言うか、それ以外のデバッグ方法がないです。

C:\> scriptcs
scriptcs (ctrl-c or blank to exit)

> #load "Hoge.csx";
> var hoge = new Hoge { Fuga = "fuga", Piyo = 1 };
> hoge
{
  "$id": "1",
  "Fuga": "fuga",
  "Piyo": 1
}
> hoge.Foo();
"Foo"
>

スクリプト内でのアセンブリの読み込み

#loadと同じ感覚で#rを使うことでGACに登録されているdllを読み込む事も出来ます。何か参照設定が足りてないな?と思ったら使いましょう。

これは例を書いても仕方ないので、GitHubのサンプルを読んでおいてください。

また、これを使うことで既存の資産であってもバリバリ呼び出すことが出来ます。GACに登録するのはちょっと面倒だけども。

[2015/02/22追記]別にGACに登録されている必要はありません。相対パスを指定してあげればdllを読み込んでくれます。

ちなみに以下のアセンブリは自動で読み込まれています。

  • System
  • System.Core
  • System.Data
  • System.Data.DataSetExtensions
  • System.Xml
  • System.Xml.Linq

また、以下のnamespaceは暗黙的にusingが指定されています。

  • System
  • System.Collections.Generic
  • System.Linq
  • System.Text
  • System.Threading.Tasks

「普通にコンソールアプリケーションを作る時の設定と一緒」と覚えておけば十分でしょう。

Nuget

ScriptCSはなんとNugetまで内蔵しています。ここにあるパッケージは全て流用可能です。

インストール方法も極めて単純です。例えばJson.NETであれば、Nugetのコマンドはこうなっているわけですが、

PM> Install-Package Newtonsoft.Json

そのまま以下の形に置き換えるだけです。

scriptcs -install Newtonsoft.Json

当然依存関係があればそれも一緒に引っ張ってきてくれます。

インストールしたパッケージは「scriptcs_packages」と言うディレクトリで管理されます。ここに保存されたものは#rも#loadも必要ありません。使いたい時にusingでnamespaceを指定すればOKです。

が、一つだけ注意しなくてはならないことがあります。scriptcs_packagesもscriptcs.exeを呼び出したディレクトリ(カレントディレクトリ)に作成/保存されますし、スクリプト内でもカレントディレクトリにscriptcs_packagesがなければそのパッケージを読み込みません。

Script Packs

ScriptCSで使用されることを目的としたパッケージ群であるScript Packsなんてものもあります。ほとんどがNugetで配布されているのでやはり簡単にインストールできます。

これらはScriptCs.Contractsを継承しており、Require<TScriptPack>()と言う形式で呼び出すことが出来ます。例を書くのが面倒なのでGitHubの例を読んでおいてください。

[2015/02/22追記]

起動時引数の設定方法

大事なことを書き忘れていました。ScriptCSでも起動時の引数を設定することができます。

先に引数の取得から説明しましょう。Env.ScriptArgsReadOnlyCollection<string>として入っています。ReadOnlyCollectionはIEnumerableを実装していないので注意しましょう。

サンプルコードとしてはこんなものでいいでしょう。test.csxとでもしておきます。

Console.WriteLine(Env.ScriptArgs[0]);

引数を渡すのはこんな感じです。

C:\>scriptcs test.csx -- test

簡単ですね。マルチバイト文字やスペースを含むような引数の場合はダブルクオーテーションでくくればOKです。

C:\>scriptcs test.csx -- "ほげほげ" "ふが ふが"

ちなみにREPLで起動する場合は引数を渡せません。なんでじゃ。

まとめ

と言うわけで、既存の資産のバリバリ使用しつつちょっとしたコードをガンガン書けるので中々に便利です。

「好きなエディタでC#を書けるよ!」を売り文句の一つにしていますが、ぶっちゃけそれは非常に辛いので実際にはCShellあたりで書くことになると思います。

C#が大好きすぎてスクリプト言語全般に死んで欲しい人は是非試してみてください。

pupを使ってコンソール上でスクレイピングする

curlとかwgetとか使っているといっそコンソール上でスクレイピングしたくなることがよくあります。

自分でパーサを書くのも面倒だし何かないかなと探していたらpupなるものを見つけました。

動作

標準入力 or ファイルの内容をCSS Selectorで検索、抽出し、標準出力に表示します。

つまり、パイプを通し放題なわけです。便利ですね。

出力形式は

  • HTML
  • テキスト
  • json

のうちどれか一つを指定することが出来ます。また、あるElementのあるAttributeの値だけを出力、なんてことも出来ます。

勿論、pupで出力した結果をパイプしてgrepしたりtrして更に加工してもいいでしょう。pupの結果をpupにパイプしたりしてもいいと思います。

インストール

Release Pageコンパイル済みの実行ファイルが置いてあります。

Go言語の環境がインストールされていればgo getで、Mac使ってるならbrewでもいいみたいです。

使い方

GitHubQuick StartExampleを見ておけば十分です。

出力形式の指定

Display Functionsと言うセクションに書いてあります。

  • text{} - 抽出したElementのテキスト
  • attr{attrkey} - 抽出したElementのattributeの値
  • json{} - 抽出したElementのjson

指定しなければHTML形式のまま出力されます。

attr{attrkey}を上手く活用すればXMLのパースもそこそこ容易に可能なので、コンソール上で動くRSSリーダーなんかも作れたりします。

オプション

オプション 省略形 動作
—color -c 出力結果を色付きで表示してくれる。
—file filePath -f filePathに指定したファイルを使用する。
—help -h ヘルプを表示する。
—indent count -i countに指定した数値の数だけインデントする。(html、jsonのみで有効)
指定しないとデフォルトのインデントが入る。
—number -n 出力された結果の番号を表示してくれる…らしいのだが、指定すると「そんなオプションはない」と怒られる。
ソースを見てみると確かにない。何だこれ。
—limit level -l level以上の要素を「…」として表示します。いっそ表示してくれないほうがありがたいんだが。
textも1レベルとしてカウントするので注意。
—charset charset N/A UTF-8EUC-JP以外のものを読み込ませる時に使うのだと思われるが、どんな値が指定できるのかよくわからない。
内部ではcharset.Lookupにそのまま値を渡しているが、これまたどんな値が指定できるのかよくわからない。
試しに「Shift-JIS」を渡したら上手く行ったので、常識の範囲内で指定してあげて下さい。
—version N/A バージョンを表示します。

まとめ

そんなわけで、pupを入れればちょっとしたスクレイピングならコンソールだけで簡単に出来てしまいます。

一々何らかの言語でパースしてた人は試してみてはいかがでしょうか。

Windowsに入れておきたいツールや環境いろいろ

最近お仕事用の新しいPC(Windows 7)を受領したんですが、一から環境を作ってたら色々面倒だったのでメモしておきます。

プログラミング言語に関する環境は特に説明しません。欲しくなったら入れるスタンス。でもWindowsならExpressでいいからVisual Studio入れておきたいなぁ…。ちょっとしたものを作るのにもC#便利なんだよなぁ…。

ユーティリティツール

QTTabBar

エクスプローラーのタブ化。お仕事中はどうしても大量のフォルダを開いて作業することがあるので…。

個人的にはCloverの方が好きなんですが、セキュリティ面で色々怪しいのでこっちを使います。

Wheel Redirector

フォーカスしてないウィンドウでもマウスホイールによるスクロールを有効化してくれます。

地味ながらも、これがあるのとないのとでは作業効率がかなり違う。

Fiddler

ユーティリティツールでもなんでもないんですが…。

日常生活で「これどんな値をPostしてるんだろう?」と気になることがたまにあるので入れておきます。

AutoHotKey

仕事中に突然キーボードが壊れた時に…。今まで一回もそんなことになったことがないですが。

色々とスクリプトも書けて便利なのはわかるんですが、それはそれで面倒なんですよね。

CLCL

クリップボード拡張。一つ入れておくと便利。

エディタ

サクラエディタ

正規表現によるFind, Replace, Grepができるテキストエディタならなんでもいいです。

特にプラグインやマクロを入れなくても一通りの機能が全部入っているのでとりあえず入れます。

IME

Google日本語入力

MS IMEなんか使う理由がありません。

Google日本語入力に変更する際にIMEの切り替えショートカットキーを無効にしておきます。

ランチャ

Executor

キーワードやコマンドを入力して起動するランチャ。キーワードを直接Googleで検索することも出来る。ファイル検索も可能。

ランチャと言うよりは、Win+Rで出せる「ファイル名を指定して実行」の強化版と捉えるといい。

デフォルトだとProgram Filesが対象になってなくて使いにくいので自分で追加するのが吉。

日本語に対応してないのが残念。日本語のフォルダ名配下のファイルとかもダメっぽい。

キーワードに追加すれば問題ないし、結構簡単にぼんぼん追加出来るので我慢しませう。

CLaunch

Executorだと色んなアプリをまとめて起動するのが面倒なのでGUIのランチャも一つ入れておきましょう。

まぁなくてもいいっちゃいいんですが…。

コンソールまわり

コマンドプロンプトはうんこなのでちゃんとコンソールの環境も整えます。

Console 2

コンソールのタブ化etc。

個人的には「わざわざレジストリをいじらなくてもフォントが変更できる」って理由で使っています。

インストーラがないので適当にProgram Filesに突っ込んだ後、パスを通しておく必要があるのが若干面倒。

日本語を入力するにはパッチを当てる必要があるので忘れないようにしましょう。

また、日本語の表示時に変な動きをする場合はコマンドプロンプトのフォントの規定値を変更します。(ラスターフォント以外にすればよさそう)

Nyaos

Windows用のUNIXライクなシェル。タブキーでパスを補完してくれたり、自分でalias切れたりと必須と言って良いレベルです。

4.xからはUnicodeもサポートしているようなんですが、Go言語の環境を別途作成する必要があるのがなんとも…。

3.xでも全然使えるのでそっちをメインに使います。でもUnicodeのサポートほしい…。

MinGW

MinGWと言うか、MSYSのUNIXコマンドが欲しいだけですが…。

インストールが案外面倒ですが、あるのとないのでは全然違うので入れておきます。

cURL

理由はなくとも一応入れておきます。ほ、ほら、インターネット経由で何かを一括ダウンロードしたくなったりするじゃないですか。

MSIでインストールすればパスも通してくれるのでありがたいですね。

フォント関連

MacType

Macを使ったことはありませんが、文字が読みやすいことはいいことです。

割と簡単に導入できる上にちょっとした設定も簡単なので入れておきます。

設定ファイルの中身はとりあえずこんな感じ。

[General]
Name=Default
Icon=..\mactray.exe,013
HookChildProcesses=1
HintingMode=2
AntiAliasMode=4
shadow=1,1,1,0x0,0,0x0
MaxHeight=150
UseMapping=0
FontLink=1
FontSubstitutes=1
WidthMode=0
FontLoader=0
GammaMode=0
GammaValue=1.2
RenderWeight=1.3
Contrast=2.2
TextTuning=-1
TextTuningR=-1
TextTuningG=-1
TextTuningB=-1
NormalWeight=1
BoldWeight=0
ItalicSlant=0
LcdFilter=2
LoadOnDemand=1
CacheMaxFaces=16
CacheMaxSizes=16
CacheMaxBytes=10485760
EnableKerning=0
BolderMode=0
MaxBitmap=0
[UnloadDll]
[exclude]
[FontSubstitutes]
[Individual]
[excludeSub]

MeiryoKe_Console

メイリオ等幅フォントConsolas用の和文フォントにします。

フォントを作成し終えたらレジストリはこんな感じにします。

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontLink\SystemLink]
"Consolas"=hex(7):6d,00,65,00,69,00,72,00,79,00,6f,00,4b,00,65,00,5f,00,36,00,\
  30,00,32,00,72,00,31,00,2e,00,74,00,74,00,63,00,2c,00,4d,00,65,00,69,00,72,\
  00,79,00,6f,00,4b,00,65,00,5f,00,43,00,6f,00,6e,00,73,00,6f,00,6c,00,65,00,\
  2c,00,31,00,32,00,38,00,2c,00,31,00,32,00,38,00,00,00,00,00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontSubstitutes]
"MS ゴシック,128"="MeiryoKe_Console,128"
"MS Pゴシック,128"="MeiryoKe_PGothic,128"
"MS Gothic,128"="MeiryoKe_Console,128"
"MS PGothic,128"="MeiryoKe_PGothic,128"
"MS UI Gothic,128"="Meiryo UI,128"

FontSubstituesはどっちでもいい気がするけどね…。

FireFoxのアドオン

ブラウザは仕事だとFireFoxを使います。理由はWeb関連のデバッグが一番やりやすいからです。それ以外の理由は特にありません。

Adblock Plus

別に広告にもトラッキングにも困ってませんが入れておきます。

FireGestures

デフォルトで入れとけよっていつも思うんですが。

FireBug

これもデフォルトであって良いレベル。

[2015/01/31追記]

パッケージマネージャ

Chocolatey

「ちょっとあの言語の環境が欲しいな…」って時に非常に便利。

リポジトリに登録されているパッケージを眺めればわかると思いますが、日本産でなければ主要なソフトウェア・アプリケーションはこれでとってこれます。

ただ、何らかのプロキシを経由する必要がある場合は結構面倒くさいです。(3時間ぐらいハマった)

まずchocolateyをインストールすることが出来ません。プロキシ経由でインストールする手順がちゃんと紹介されているので、これを粛々と読み、そのようにしましょう。

が、インストールし終わっても、パッケージをインストールしようとするとまたプロキシの壁に阻まれることがあります。

私がハマったケースではどうやらChocolateyが内部で使用しているNuGetのプロキシ設定が上手く行っていない…というか、そもそもされていない感じだったので、設定します。

デフォルトだと「C:\ProgramData\chocolatey\chocolateyinstall」に件のNuGet.exeがいるので、こいつに対してプロキシのconfigをブチ込んでいきます。

設定方法はこんな感じ。

NuGet.exe config -Set http_proxy=http://my.proxy.address:port
NuGet.exe config -Set http_proxy.user=mydomain\myUserName
NuGet.exe config -Set http_proxy.password=myPassword

まとめ

とりあえずはこんなもんでしょうか。

後はまぁ、必要になったら適宜入れれば十分でしょう。

【Java】あなたと Java と Enum

2019/01/02 追記

  • シンタックスハイライトを適用しました。
  • 大幅な加筆を行いました。
    • いくつかのサンプルコードを Java8 以降の内容に書き直しました。

前書き

お久しぶりです。

お久しぶりですと言っても、定期購読している人は恐らく数人しかいないんじゃないでしょうか。技術ブログなんてそんなものです。

別に忙しかったわけじゃないんですが…いや…忙しかったのかな。忙しかったんですが色々遊んでもいました。単にネタにするものがなかっただけです。

最近は 2〜3 年ぐらい前に作成して 1 年間ほったらかしにしていた Android アプリの改修をゴリゴリやっています。

とりあえず機能追加は一切しないでリファクタリングと簡単な障害対応メインで…と思ったらあまりのクソコードさに失禁しかけたので、ほとんど全コード書き直しています。しかし、2〜3 年前の自分を殺したくなるのは、エンジニアにとってはいい兆候です。(ポジティブ)

さて、今回はAndroid のパフォーマンスに関する手引きで「使ったらぶっ殺すからな」と言われていた Enum に関する記述がいつの間にか消えていたので、Java での Enum に関するあれこれを書いていきます。

JavaEnum のしくみと使い方

JavaEnumJDK でもあんまり使われていません。まぁこれは Java 1.5 からの機能なので、互換性的な問題があるからかもしれません。

しかし、色々な現場でも、ライブラリでも、使われているのを見ると「おっ、Enum が使われているぞ!?」って気分になります。C#なんかはどこでもバリバリ使っていて非常に楽なんですが。

Java であんまり使われない理由、そして Android で忌避されていた一番大きな理由は、あまりにもリッチすぎるからだと思われます。

とりあえず Enum の宣言方法の紹介も兼ねて適当なコードを載せておきましょう。

public enum TestEnum {
    TEST1,
    TEST2,
    TEST3, //最後がカンマで終わっていてもOK
}

基本的にはこんな感じです。応用すると色々なことが出来すぎてしまうんですが、これは後で説明します。

上記のコードをコンパイルすると内部でこんなコードに変換されます。

final class TestEnum extends Enum<TestEnum> {
    private TestEnum(String name, int ordinal) {
        super(name, ordinal);
    }

    // コンストラクタのnameには列挙子の名前、ordinalには宣言された順番が入る
    public static final TestEnum TEST1 = new TestEnum("TEST1", 0);
    public static final TestEnum TEST2 = new TestEnum("TEST2", 1);
    public static final TestEnum TEST3 = new TestEnum("TEST3", 2);

    private static final TestEnum ENUM$VALUES[] = { TEST1, TEST2, TEST3 };

    // 他にもvalueOf(String name)やvalues()のようなstaticメソッドが生成されるが割愛

}

と言うわけで、Enum を宣言すると中の列挙子の分だけインスタンスが作成されます。static finalなので==での比較が可能になるわけです。

TestEnum en = TestEnum.TEST1;
if (en == TestEnum.TEST1) {
    // もちろんen.equals(TestEnum.TEST1)でもtrueを返す
}

Enum もオブジェクトなので宣言時にnullを入れておくことが出来ますが、なるべく初期値用の列挙子を用意しておくべきです。

と言うのも、Enumswitchの condition として指定した変数がnullだとNullPointerExceptionが発生するからです。

public enum TestEnum {
    TEST1,
    TEST2,
    TEST3,
    UNKNOWN,
}
// Enum に null を指定
TestEnum en = null;

// switch の condition に使用すると NullPointerException が発生する
switch(en) {
case TEST1 :
break;
case TEST2 :
break;
case TEST3 :
break;
default :
break;
}
// そこで、初期値用の列挙子「UNKNOWN」を用意しておく
TestEnum en = TestEnum.UNKNOWN;

switch(en) {
case TEST1 :
break;
case TEST2 :
break;
case TEST3 :
break;
case UNKNOWN : // 初期値用の列挙子の場合は default にフォールスルーしておけばよい
default :
break;
}

そもそも何でswitchNullPointerExceptionが発生するかなんですが、内部で Enum#ordinal()を呼び出しているからです。

また、上記の記事を読めばわかる通り、各ラベル内で自動で try-catch を行います。そのため、Enum の switch は普通の switch よりもコストが高い、と言うことは覚えておきましょう。

Enum の値を定義する

いきなりデメリットばかり取り扱ったせいでこんなクソ機能いらないでしょ、と思うかもしれませんが、そんなことはありません。色々工夫すると非常に強力な機能に化けます。

JavaEnum は各言語の Enum 同様、列挙子に値を定義することが出来ます。ちょっと変わった形ですが、こんな感じです。

public enum TestEnum {
    // 値を定義する場合は最後がセミコロンで終わってないといけない
    TEST1(0),
    TEST2(1),
    TEST3(2);

    private final int value;

    // privateスコープでコンストラクタを定義する
    // (* public, protected, package-privateではダメ。
    //    新しいインスタンスを外部から勝手に生成されたらEnumのメリットがなくなるので…。)
    private TestEnum(int value) {
        this.value = value;
    }

    public int getValue() {
        return this.value;
    }
}

先ほどの話のおさらいになりますが、結局のところ Enum の列挙子はクラスのインスタンスです。だから、コンストラクタで初期値を渡してもそれは Java の仕様内の話になるので合法です。

初期値の型に制限はありませんし、コンストラクタに渡す引数の数にも制限はありませんし、コンストラクタのオーバーロードがいくつあろうと構いません。だってそれは Java の仕様内の話だから。

値を返すメソッド etc や値を保持しておくフィールドは自分で定義しなきゃいけませんが、まぁ、そんなのLombok使っておけばいいじゃないですか。

また、EnumSerializableを実装しているので、保持する値もSerializableであれば、そのままシリアライズ / デシリアライズが可能です。

今回の例ではvalueを final にしたり、setter を用意したりしませんでしたが、どちらも制限があるわけではないので、Enum なのに何らかのステートを持たせるなんてことも可能です。可能ですが、それはもう一般的な Enum からは逸脱しすぎている気がします。

Enum にメソッドを定義する

何度でも言いますが、Enum の列挙子はクラスのインスタンスです。クラスに対しメソッドを定義しても誰にも怒られません。って言うかさっきの例で思いっきり getter 書いてるし。

ちょっとエキセントリックなんですが、Enumabstractメソッドを定義することも出来ます。じゃあそのabstractなメソッドはどこで実装するの?と言うと、宣言時です。

public enum TestEnum {
    TEST1 {
        @Override
        public String test() {
            return "TEST1";
        }
    },
    TEST2 {
        @Override
        public String test() {
            return "TEST2";
        }
    },
    TEST3 {
        @Override
        public String test() {
            return "TEST3";
        }
    };

    public abstract String test();
}

いわゆる匿名(無名)クラス的な扱いになるわけですね。この機能をフル活用すると、Enum なのにswitchで分岐せずabstractなメソッドを呼ぶだけ、とかそんなことも出来ます。*1

巷でテンプレートパターンと呼ばれているものはすべて Enum で代替可能です。代替すべきかどうかはおいといて。

また、メソッドが定義できると言うことは、インターフェースを実装することも可能だと言うことになります。Enum なのに別インターフェースとしても振舞えるわけです。気持ち悪いですね。

ただし、他のクラスを継承することはできません。なぜなら、すべての Enum は暗黙的にEnum<E extends Enum<E>>を継承しているからです。二重継承になってしまいます。

文字列と Enum の相互変換

個人的に Enum の一番の強みはここだと思っています。文字列 ⇔ Enum への変換がめちゃくちゃ楽です。

特に JSONXML をパースする時に恐ろしい力を発揮します。

文字列 → Enum

文字列から Enum へ変換するにはvalueOfメソッドを使用します。これには javaDoc がありません。なぜなら、コンパイル時に自動生成されるstaticメソッドだからです。

// TestEnum.TEST1を文字列から生成する
TestEnum en = TestEnum.valueOf("TEST1");

Enum#valueOf(Class<T>, String)なんてものもありますが、リフレクションでもしない限りは使わないと思います。

Enum → 文字列

これには二通りの方法があります。Enum#name()を使用する方法と、Enum#toString()を使用する方法です。

違いは非常に簡単で、#name()はオーバーライド不可、#toString()はオーバーライド可能なメソッドです。

public enum TestEnum {
    TEST1,
    TEST2,
    TEST3 {
        @Override
        public String toString() {
            return "TEST3だよ";
        }
    };
}
System.out.println(TestEnum.TEST1.name())
// => TEST1
System.out.println(TestEnum.TEST2.name())
// => TEST2
System.out.println(TestEnum.TEST3.name())
// => TEST3

System.out.println(TestEnum.TEST1.toString())
// => TEST1
System.out.println(TestEnum.TEST2.toString())
// => TEST2
System.out.println(TestEnum.TEST3.toString())
// => TEST3 だよ

いろんな値 ⇔ Enum

自分でメソッドを作ればいいじゃない。(完)

public enum TestEnum {
    TEST1(0),
    TEST2(1),
    TEST3(2),
    UNKNOWN(-1);

    private final int value;
    private TestEnum(int value) {
        this.value = value;
    }

    public int getValue() {
        return this.value;
    }

    public static TestEnum valueOf(int value) {
        // 暗黙的に定義されるvalues()メソッドを使うことで全列挙子を取得することが出来る
        return Arrays.stream(TestEnum.values())
            .filter(e -> value == e.getValue())
            .findFirst()
            .orElse(TestEnum.UNKNOWN);
    }
}

Enum のためのコレクション

EnumMap

Enum には専用のMap実装が用意されています。その名もEnumMapです。そのまんまじゃないか。

使い方はHashMapとほとんど変わりません。ただ、Key の比較にEnum#ordinalを使用しているので、HashMapより速いです。

しかしまぁ、Enum 側でいくらでも紐付くプロパティを定義出来るので、実はそんなに使う機会がないと言う…。存在自体を忘れないようにしたいですね。(自戒)

EnumSet

EnumSetなるものも用意されています。

これはstaticメソッドからのみインスタンスを生成することが出来ます。よく使いそうなものだけ挙げておきます。

そもそもある Enum の全列挙子を取得したいのであればEnum#values()を使用すればいいのですが、Enum を使ってビット操作のようなことがしたい場合はこれを使うといいよ、と言われています。

ドキュメントには

従来の int ベースの「ビットフラグ」に対する高品質かつ型保証された代替として十分に使用可能です。

と書いてあるものの、そもそもビット操作をしなきゃいけない時点で Enum なんて使わなそうだし、そもそも Java を(省略)

具体的にどうやってビット操作の真似事をするかと言うと、Setadd / removeを使ってフラグの上げ下げをするだけです。なんだそれ。

Enum 共通のdefault / staticメソッドを定義する

コード値との相互変換

DB のカラムに格納する値だとか、Web なら<select>で使うような値を Enum にしておくと色々捗ります。

少なくとも変換用のマジックナンバーを用意したり、constant な値だけを定義したクラス or インターフェースを用意するよりよっぽど簡単かつ便利です。自然とモジューラブルになるので、保守性も高いです。

(色々面倒なのでLombok使用前提で…。)

/**
 * 性別
 */
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public enum Gender {
    MALE("男", "1"),
    FEMALE("女", "2"),
    OTHER("その他", "3"),
    UNKNOWN("", "9");

    private final String label;
    private final String code;

    public static Gender ofCode(String code) {
        return Arrays.stream(Gender.values())
            .filter(e -> e.getCode().equals(code))
            .findFirst()
            .orElse(Gender.UNKNOWN);
    }
}

で、実際に作って行くと、ofCodeメソッドを共通化したくなってくることがあると思います。

先述した通り Enum は暗黙的にEnum<E extends Enum<E>>を継承しているため、共通処理を定義する場合はインターフェースを作るしかありません。

Enum 共通処理を定義したインターフェース

幸い Java8 からはインターフェースにdefault / staticメソッドを定義できるので、クラスを継承できなくても何とかなります。

public interface CodableEnum<E extends Enum<E>> {
    String getCode();

    @SuppressWarnings("unchecked")
    static <E extends Enum<E>> CodableEnum<E> ofCode(String code, CodableEnum<E> orElse) {
        return Arrays.stream(orElse.getClass().getEnumConstants())
                .filter(e -> e.getCode().equals(code))
                .map(e -> (CodableEnum<E>) e)
                .findFirst()
                .orElse(orElse);
    }
}

ちょっと面倒なのはEnum#values()が使えないことでしょうか。代わりにClass#getEnumConstants()を利用して Enum の一覧を取得する必要があります。

後は先程作った Enum に実装してやれば OK です。

@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public enum Gender implements CodableEnum<Gender> {
    MALE("男", "1"),
    FEMALE("女", "2"),
    OTHER("その他", "3"),
    UNKNOWN("", "9");

    private final String label;
    private final String code;
}
final Gender gender = Gender.ofCode("1", Gender.UNKNOWN);
// -> Gender.MALE が返される

まとめ

とまぁ、アイデア次第で色々と出来てしまいます。

public static finalなフィールドが多すぎてやってられなくなったりしてきたら、Enum を使って大富豪的プログラミングに興じてみるのはいかがでしょうか。

参考

*1:Template パターンのようなものだと考えれば良い。

【Atom】やっておきたい設定+入れておきたいパッケージメモ

記事を読む前に

この記事は非常に古いです。【Atom】入れておきたいパッケージメモ2015年版と言う記事を新たに書いたので、そっちを読んで下さい。

前書き

最近Atomを入れました。

入れたはいいんですが、なんかこう、微妙です。とても。まず名前がイケてないよね。Atomて。データ形式かよって。

とは言え、プロジェクト管理機能がついてるテキストエディタが一つ欲しかったのでまぁそれなりに頑張って使ってみようと思います。使ってみようとは思うんですが、デフォルトのままだと普通に使いにくいので適当に環境が復元できるようメモしておきます。

って言うか、プロジェクト管理機能が必要ない文章ならサクラエディタで十分なんですけどね。Markdownのプレビューが出来るって言われてもテーブル書いた時ぐらいしか確認したくないし…。

覚えておきたいショートカット

Ctrl + , - 設定画面

「パッケージ動かねーぞボケが!」と言った事象から「パッケージ動かねーぞボケが!」と言った事象まで様々なことを解決するために頻繁に呼び出します。

正直、一番使っているショートカットが恐らくこれなんですが、それはそれでどうなんでしょう?私はコーディングがしたいのであって設定がしたいわけじゃないんですが。

Ctrl + Shift + P - コマンドパレット

やりたいことはなんとなくわかってるんだけどどうすればいいのかわからないときに使うショートカット。それなりに解決するが、たまに解決しない。

Ctrl + Shift + F - プロジェクト単位のFind and Replace

開いているプロジェクト内のファイルを一括でgrepしてくれます。

Replace後のプレビューも自動で表示されてくれるのが中々ありがたいです。終わった後Escするのが面倒ではありますが。

後は特にない…と言うか、追加したパッケージに設定されているショートカットばっかりなので、この二つを覚えておけば大概どうにかなることがわかりました。

入れないと辛すぎるパッケージ

勿論個人の感想です。

Project Manager

頻繁に使うプロジェクトを登録することが出来ます。

保存方法は「プロジェクトを開く」→[Packages]→[Project Manager]→[Save Project]

呼び出すときはCtrl + Shift + Alt + P

Japanese Wrap

これを入れないと日本語の禁則処理をまともに行ってくれません。

ただまぁ、日本語の長文を書きたいならサクラエディタの方がよっぽど便利なんですが、それを言ってしまったらおしまいです。

Docblockr

ブロックコメントを自動生成してくれます。

かなり便利…と言うか、これなしでブロックコメントを書こうとすると死ぬほど辛いです。

Save Session

前回終了時に開いていたプロジェクトやファイルを自動で再開してくれます。

基本入れておいて損はないです。Atomを何かの間違いでメモ帳代わりに使うつもりがあるならおすすめしませんが。

入れておくとそこそこ便利なパッケージ

Autocomplete Plus

デフォルトのコード補完はCtrl + Spaceを押さないと出てこないんですが、これを入れると入力中にバンバン候補をだしてくれます。

あまりにもバンバン出すぎて邪魔って言うか、お前人がコメント書いてるときに出てくんなよって言うか、plain textでも出てくるとかいい加減にしろよとか、候補と完全一致する単語を入力すると候補からその単語が消えて全く別の単語がサジェストされるのは流石にどうかしているとか、言いたいことは色々あるんですが、まぁあると便利です。

MinimapMinimap Find And Replace

コードのミニマップをちょろっと出してくれます。

これ自体は便利ともどうでもいいともなんとも言えないんですが、周辺プラグインのMinimap Find And Replaceは中々便利です。

Git Plus

元々AtomはGitに中途半端に対応しているんですが、中途半端すぎていらいらしてくるので、精神衛生上これを入れておくのが無難だと思われます。

Command Toolbar

「たまに使うんだけどキーバインドを覚えるほどではない」みたいなものを登録しておくと捗ります。

わりかしどうでもいい or 趣味的なもの

File Icons

アイコンがちょっとオシャレになります。デフォルトよりはファイルが一目で見分けられるので意外と便利です。

VS Syntax Theme

Visual Studio風のSyntax Theme。テーマなのでパッケージではないんですが。

Visual Studio風を名乗っている癖にデフォルトのフォントがConsolasじゃない」と言う理由でissueないしはpull requestを送ろうかと思ったんですが、自分で勝手に改変できるのでやめておきました。

おまけ - フォントの変え方とか

SettingsのFont Familyを変えればいいとか、styles.lessを書き換えればいいとか、諸説あるんですが、一番手っ取り早いのはThemeをAtomで開いてfont-familyを設定している箇所をgrep、自分の好きなフォントに差し替える、が一番早いです。

早いですって言うか、それ以外だとThemeに上書きされてしまうっぽいですね。

デフォルトの日本語フォントはちょっとひどすぎるので、それなりのフォントに差し替えないと段々生きるのが辛くなってきます。一番最初に直してしまいましょう。

Themeに限らず、パッケージで「なんだこのクソみたいなキーバインドは」と言うような、何らかの不満を抱いたらSettingsを開き[Filter package]から該当パッケージを選択、[Open in Atom]ってボタンを押せば好き勝手編集できてしまうのでさっさとそうしましょう。

おまけその2 - 高速化

なんか起動がクソ重いんだけど!と思ったらコマンドパレットを開き、Timeと打ち込んでみましょう。TimeCop: Viewなるものが出てくるはずです。

TimeCopくんは起動にかかった総時間や各プラグインの起動時間を表示してくれます。あんまり恩恵を享受できてないのに起動がクソ重いプラグインなんかはさっさとアンインストールしてしまいましょう。

また、Settings -> Editor Settingsの「Use Hardware Acceleration」あたりのチェックを外すといきなり爆速になったりします。この辺は環境によって違うと思いますが、色々試してみるにこしたことはありません。

まとめ

この文章はサクラエディタで書きました。

【Android】Volley各種設定メモ

ちょっとVolleyを使いたい要件があるんですが、公式のドキュメントを読んでも「いや知りたいのはそこじゃねーよ」と言うものばかりなのでメモしていきます。

JavaDocこれを参考にしています。正直このJavaDocが生成されたのがいつ時点のビルドかわからないのでもっと色々追加されてるのかもしれないんですが、ちょっとそこまで追う気力はないです。

覚えておきたい前知識

重要なオブジェクトはこの辺です。

RequestQueueはその名の通りRequestをキューイングするクラスです。ExecutorServiceみたいなものだと思っておけばいいです。

Request<T>もその名の通りHTTPリクエストに関する諸々を設定するクラスです。URLだとか、GET/POSTメソッドだとかをコンストラクタで指定します。

deliverResponseメソッドは明らかにレスポンスに関わる部分なんですが、まぁここにあったほうが便利ですね。

NetworkはRequestを受け取って実行しNetworkResponseを返す、いわばアダプタです。実装はBasicNetworkが用意されていますし、これで十分です。

と言うか、Volley#newRequestQueue経由でインスタンスを作成するとNetworkの指定すらできません。

HttpStackはBasicNetworkで使用される、実際の通信部分です。実装としてはHttpClientStackとHurlStackが用意されています。Hurlは「HttpURL」の略だと思われます。

HurlStackはperformRequest内でHttpURLConnectionを作成しているんですが、Gingerbread(API Level 9)より前はHttpUrlConnectionにバグがあるので、minSdkVersionによってはちゃんと分岐させないと痛い目を見そうです。

Header / Post paramsの設定はRequest<T>のメソッドをオーバーライドする

HeaderとかPostのparamとか追加したいと思うんですが、その手のものはRequestのgetterをオーバーライドすることで渡せます。まぁ、1リクエスト毎に渡せた方がありがたいので、そうですねって感じ。

使いたいRequestをnewする際にオーバーライドするだけでもいけます。

RequestのJavaDoc眺めてsetterがなかったらそいつ、って認識。具体的にはこの辺のメソッド

エンコード指定が必要ならこの辺も。特にgetParamsEncodingはデフォルトだとUTF-8を返すようになってるので注意したほうが良さそうです。

リトライやタイムアウト設定はRetryPolicyを使う

タイムアウトの設定なんかはRequest#setRetryPolicyRetryPolicyを渡せばOKです。

DefaultRetryPolicyのコンストラクタが便利です。デフォルト値はソース見たほうが早いです。

自己署名証明書によるHTTPS通信設定

HurlStackを使うかHttpClientStackを使うかで微妙に変わってきます。

細かい設定方法なんかは本旨にそぐわないので割愛。(めんどくさいんだよねあれ…。)

HurlStackを使う場合

コンストラクタでSSLSocketFactoryを渡せるので渡してあげましょう。

参考:Android - Volleyで自己署名証明書を使ったHTTPS通信 - Qiita

HttpClientStackを使う場合

どっちにしたってコンストラクタでHttpClientを渡さなきゃいけないのでいつも通りにやればOK。

参考:

Cookieの設定とか取得とか

Cookieを使用する場合はRequestQueueのthreadPoolSizeを1にする必要があるらしいので、気をつけましょう。クソ面倒ですね。

設定も取得もCookieManagerを使うのがベストでしょう。リクエストを飛ばした後のHttpClientが手に入るならCookieStoreも併用したいところですが、残念ながらこれは手に入りません。レスポンスヘッダからちまちま取得します。

具体的にはこんなコードをRequest内で書けばOK。

public abstract class CookieRequest<T> extends Request<T> {
    
    // コンストラクタの実装とかは省略
    
    // Cookieの設定
    @Override
    public Map<String,String> getHeaders() throws AuthFailureError {
        Map<String,String> header = null;
        
        CookieManager cm = CookieManager.getInstance();
        // 設定されたURLからCookieが取得できないか試行する
        String cookie = cm.getCookie(getUrl());
        
        if(cookie != null) {
            header = new HashMap<String, String>();
            String[] cookies = cookie.split(";");
            for(String c : cookies) {
                c = c.trim();
                String[] cs = c.split("=");
                // ヘッダにCookieを詰め込む
                header.put(cs[0], cs[1]);
            }
        }
        
        return header != null : header ? Collections.emptyMap();
    }
    
    // Cookieの取得
    @Override
    protected Response<T> parseNetworkResponse(NetworkResponse response) {
        Map<String, String> headers = response.headers;
        if(headers != null) {
            CookieManager cm = null;
            for(Entry<String, String> e : headers.entrySet()) {
                if(!"Set-Cookie".equars(e.getKey()))
                    continue;
                
                if(cm == null) cm = CookieManager.getInstance();
                
                cm.setCookie(getUrl(), e.getValue());
            }
        }
        
        return super.parseNetworkResponse(response);
    }
}

OKと言ったもののまだ動かしてないので本当にこれでいいのか微妙です。そのうち試します。

まとめ

HttpClient / HttpURLConnectionを後から変更できるようにしてくれ頼む。

参考

【JavaScript】marked.jsを無理矢理拡張してオレオレパーサーを作る

最近例のクソアプリのマニュアルを書いているんですが、なんだかHTMLを書くのが面倒になってきました。

GitHub Pagesですし、jekyllを使えば比較的簡単にMarkdown記法でガリガリ書けるらしいんですけど、使い方を覚えるのが面倒です。また、Rubyの環境がないと書けないのもちょっと嫌です。

どうしようかなぁと色々探してみたらmarked.jsなるものを使えばとても簡単にMarkdownをHTMLに変換してくれると言うことを知り、じゃあ使ってみようか、となったわけです。

が、そのまま使うとなると色々と面倒なことがあるので、無理矢理拡張してオレオレパーサーにしてしまいました。

marked.jsの使い方

非常に簡単です。コード例を読むだけでも十分でしょう。

<!doctype html>
<html>
<head>
  <meta charset="utf-8"/>
  <title>Marked in the browser</title>
  <script src="lib/marked.js"></script>
</head>
<body>
  <div id="content"></div>
  <script>
    document.getElementById('content').innerHTML =
      marked('# Marked in browser\n\nRendered by **marked**.');
  </script>
</body>
</html>

marked.jsを読み込んだ後、適当なStringをmarked関数に渡せば変換してくれます。

もちろん適当なStringは適当なmdファイルとして別途保存しておき、XHRで読み込んでもいいでしょう。と言うか、HTMLに埋め込む方が辛いので、普通はそうすると思います。

問題点

とりあえず適当なページのHTMLを紹介しておきましょう。

<!DOCTYPE html PUBLIC "">
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="chrome=1">
    <meta name="description" content="Tumbling-dice.github.io : ">
    <link href="../stylesheets/stylesheet.css" rel="stylesheet" type="text/css" media="screen">
    <script src="../javascripts/jquery-1.11.1.min.js" type="text/javascript"></script>
    <script src="../javascripts/create-index.min.js" type="text/javascript"></script>
    <title>包丁で刺されるはたてちゃんアラーム マニュアル - Twitter連携アカウント管理画面(AccountListActivity)</title>
</head>
<body>
    <!-- HEADER -->
    <div class="outer" id="header_wrap">
        <header class="inner">
          <a id="forkme_banner" href="https://github.com/tumbling-dice/Hatate">View on GitHub</a>
          <h1 id="project_title"><a href="../index.html">包丁で刺されるはたてちゃんアラーム</a></h1>
          <h2 id="project_tagline">マニュアル - Twitter連携アカウント管理画面(AccountListActivity)</h2>
        </header>
    </div>
    <!-- MAIN CONTENT -->
    <div class="outer" id="main_content_wrap">
        <nav class="inner" id="sidebar">
            <ol id="index">
                <li><a href="#layout">レイアウト</a></li>
                <li><a href="#guide">利用者向けガイド</a>
                    <ol>
                        <li><a href="#guide-select">アカウント選択</a></li>
                        <li><a href="#guide-add">アカウント追加</a></li>
                        <li><a href="#guide-delete">アカウント削除</a></li>
                    </ol>
                </li>
                <li><a href="#summary">(開発者向け)機能概要</a></li>
                <li><a href="#source">(開発者向け)ソース</a></li>
            </ol>
        </nav>
        
        <section class="inner" id="main_content">
            
            <div id="layout"><h3><a name="layout" href="#layout" class="anchor"><span class="octicon octicon-link"></span></a>
            1.レイアウト</h3>
                <p style="margin-left: 1em;">TODO:stub</p>
            </div>

            <div id="guide"><h3><a name="guide" href="#guide" class="anchor"><span class="octicon octicon-link"></span></a>
            2.利用者向けガイド</h3>
                <div id="guide-select" style="margin-left: 1em;"><h4>2-1.アカウント選択</h4>
                    <p style="margin-left: 1em;">チェックボックスにチェックが入っているアカウントはすべてTwitter連携対象アカウントになります。</p>
                    <p style="margin-left: 1em;">連携対象からはずしたい場合はチェックをはずせばOKです。</p>
                </div>

                <div id="guide-add" style="margin-left: 1em;"><h4><a name="guide-add" href="#guide-add" class="anchor"><span class="octicon octicon-link"></span></a>
                2-2.アカウント追加</h4>
                    <p style="margin-left: 1em;">オプションメニューから「追加」を選択することで連携対象にしたいアカウントを追加することが出来ます。</p>
                </div>

                <div id="guide-delete" style="margin-left: 1em;"><h4><a name="guide-delete" href="#guide-delete" class="anchor"><span class="octicon octicon-link"></span></a>
                2-3.アカウント削除</h4>
                    <p style="margin-left: 1em;">もう使用したくないアカウントをロングタップすると削除確認ダイアログが表示されます。</p>
                    <p style="margin-left: 1em;">問題なければOKをクリックして削除してください。</p>
                </div>
            </div>

            <div id="summary"><h3><a name="summary" href="#summary" class="anchor"><span class="octicon octicon-link"></span></a>
            3.(開発者向け)機能概要</h3>
                <p style="margin-left: 1em;">TODO:stub</p>
            </div>
        
            <div id="source"><h3><a name="source" href="#source" class="anchor"><span class="octicon octicon-link"></span></a>
            4.(開発者向け)ソース</h3>
                <ul>
                    <li><a href="https://github.com/tumbling-dice/Hatate/blob/master/src/inujini_/hatate/AccountListActivity.java">AccountListActivity.java</a></li>
                    <li><a href="https://github.com/tumbling-dice/Hatate/blob/master/res/layout/activity_list.xml">activity_list.xml</a></li>
                </ul>
            </div>
        
        </section>
    </div>
    <!-- FOOTER  -->
    <div class="outer" id="footer_wrap">
        <footer class="inner">
        <p>Published with <a href="http://pages.github.com">GitHub Pages</a></p>
        <p id="last_update">Last Update: 2014/10/28</p>
      </footer>
    </div>
</body>
</html>

このHTMLをvue.jsを使って一種のテンプレート化してしまいます。

mdファイルの名前をクエリで渡すだけで使い回せるようにするためです。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset='utf-8'>
    <meta http-equiv="X-UA-Compatible" content="chrome=1">
    <meta name="description" content="Tumbling-dice.github.io : ">
    <link rel="stylesheet" type="text/css" media="screen" href="../stylesheets/stylesheet.css" />
    <script type="text/javascript" src="../javascripts/jquery-1.11.1.min.js"></script>
    <script type="text/javascript" src="../javascripts/vue.js"></script>
    <script type="text/javascript" src="../javascripts/marked.min.js"></script>
    <title>包丁で刺されるはたてちゃんアラーム マニュアル - {{title}}</title>
</head>
<body>
    <!-- HEADER -->
    <div id="header_wrap" class="outer">
        <header class="inner">
          <a id="forkme_banner" href="https://github.com/tumbling-dice/Hatate">View on GitHub</a>
          <h1 id="project_title"><a href="../index.html">包丁で刺されるはたてちゃんアラーム</a></h1>
          <h2 id="project_tagline">マニュアル - {{title}}</h2>
        </header>
    </div>
    <!-- MAIN CONTENT -->
    <div id="main_content_wrap" class="outer">
        <nav id="sidebar" class="inner" v-html="index"></nav>
        <section id="main_content" class="inner" v-html="content"></section>
    </div>
    <!-- FOOTER  -->
    <div id="footer_wrap" class="outer">
        <footer class="inner">
        <p>Published with <a href="http://pages.github.com">GitHub Pages</a></p>
        <p id="last_update">Last Update: {{lastUpdate}}</p>
      </footer>
    </div>
</body>
</html>

後はハイライトした部分の情報をMarkdown形式で記述すれば…と思ったんですが、ある部分はサイドバーに、またある部分は本文に、またある部分はタイトル、最終更新日として…と考えていくと、1ファイルに収めるのが非常に難しいことに気づきます。

かと言ってバカ正直に別ファイルに分けてしまうと管理が非常に面倒です。ここをどうにかしていきましょう。

Rendererの動作をオーバーライドする

marked.jsのオプションとして「Overriding renderer methods」なんてものが紹介されています。

Markdown記法をパースし、HTMLに変換する瞬間のメソッドを上書きできるようです。

// サーバサイドだとこれが必要
// var marked = require('marked');
var renderer = new marked.Renderer();

renderer.heading = function (text, level) {
  var escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');

  return '<h' + level + '><a name="' +
                escapedText +
                 '" class="anchor" href="#' +
                 escapedText +
                 '"><span class="header-link"></span></a>' +
                  text + '</h' + level + '>';
},

console.log(marked('# heading+', { renderer: renderer }));
<h1>
  <a name="heading-" class="anchor" href="#heading-">
    <span class="header-link"></span>
  </a>
  heading+
</h1>

具体的にどのようなイベントをフックできるかは「Block level renderer methods」と「Inline level renderer methods」を見ておきましょう。

あくまでレンダリングのイベントなのでHTMLタグを自前で作らなきゃいけないんですが、デフォルトではどうやってるのかはソースを見ないとわかりません。

2014/10/30現在だと749行目あたりからRendererのprototypeを設定しています。その辺を掘っていけばいいでしょう。

これを上手く活用するとちゃんとしたMarkdown記法でありながら全然関係ないHTMLを返せるばかりか、特定のパターンの場合は一切レンダリングしないでテキストだけ受け取るなんてことも可能です。

タイトルでは「拡張」とか「パーサー」とか言ってますが、全くの嘘偽りであり、「marked.jsのRendererのメソッドをオーバーライドしてオレオレレンダリングを行う」が正しいです。

実際にやってみる

先ほどのHTMLの内容をMarkdown記法に書き換えました。

#Twitter連携アカウント管理画面(AccountListActivity)

<index>
1. [レイアウト](#layout)
2. [利用者向けガイド](#guide)
    1. [アカウント選択](#guide-select)
    2. [アカウント追加](#guide-add)
    3. [アカウント削除](#guide-delete)
3. [(開発者向け)機能概要](#summary)
4. [(開発者向け)ソース](#source)
</index>

##layout
TODO:stub

##guide

###guide-select
チェックボックスにチェックが入っているアカウントはすべてTwitter連携対象アカウントになります。

連携対象からはずしたい場合はチェックをはずせばOKです。

###guide-add
オプションメニューから「追加」を選択することで連携対象にしたいアカウントを追加することが出来ます。

###guide-delete
もう使用したくないアカウントをロングタップすると削除確認ダイアログが表示されます。

問題なければOKをクリックして削除してください。

##summary
TODO:stub

##source
* [AccountListActivity.java](https://github.com/tumbling-dice/Hatate/blob/master/src/inujini_/hatate/AccountListActivity.java)
* [activity_list.xml](https://github.com/tumbling-dice/Hatate/blob/master/res/layout/activity_list.xml)

[LastUpdate](2014/10/30)

実際にこれをパースしてViewModelを作ってみましょう。

$(document).ready(function () {
    // クエリからmdファイルの名前を取得する
    var query = window.location.search;
    query = query.substr(1, query.length);

    var queryItems = query.split("&");

    var fileName;

    for (var i = 0, length = queryItems.length; i < length; i++) {
        var item = queryItems[i].split("=");
        var key = item[0];

        if (key == "q") {
            fileName = item[1] ? window.decodeURIComponent(item[1]) : undefined;
            break;
        }
    }

    $.ajax({
        url: "./" + fileName + ".md"
    }).success(function (data) {

        var renderer = new marked.Renderer();

        var idx;
        renderer.html = function (html) {
            // indexタグがなかったらそのまま
            if (html.indexOf("<index>") == -1) {
                return html;
            }

            // indexタグで囲まれた部分をちゃんとパースする
            idx = marked(html.replace("<index>", "").replace("</index>", ""));
            
            // レンダリングする必要がなければ空文字を返せばOK
            return "";
        };

        var lastUpdate;
        var indexes = {};

        renderer.link = function (href, title, text) {
            // textが「LastUpdate」だったら最終更新日を取得する
            if (text == "LastUpdate") {
                lastUpdate = href;
                return "";
            }

            // hrefが#から始まる場合はナビゲーションなので、
            // 見出しを保存しておく
            if (href.substr(0, 1) == "#") {
                indexes[href] = text;
            }

            return "<a href=\"" + href + "\" title=\"" + title + "\">" + text + "</a>";
        };

        var projectTagline;

        renderer.heading = function (text, level) {
            switch (level) {
                case 1:
                    // 見出しレベルが1だったらタイトル
                    projectTagline = text;
                    return "";
                case 2:
                case 3:
                    // 見出しレベルが2〜3
                    // かつ
                    // ナビゲーションのhrefと同じtextだったら
                    // ナビゲーション用のアンカーを作成する
                    var hash = "#" + text;
                    if (indexes[hash]) {
                        // ナビゲーションのタイトルはハッシュから取得する
                        var idxTitle = indexes[hash];

                        var $header = "<h" + (level + 1) + ">";
                        $header = $header + "<a name=\"" + text + "\" href=\"" + hash + "\" class=\"anchor\">"
                                          + "<span class=\"octicon octicon-link\"></span></a>"
                                          + idxTitle
                                          + "</h" + (length + 1) + ">";

                        return $header;
                    }

                    return "<h" + level + ">" + text + "</h" + level + ">";

                default:
                    return "<h" + level + ">" + text + "</h" + level + ">";

            }
        };

        renderer.paragraph = function (text) {
            return "<p style=\"margin-left:1em;\">" + text + "</p>";
        };

        // markedの第三引数にcallbackを設定できる
        marked(data, { renderer: renderer }, function (err, content) {
            new Vue({
                el: "html",
                data: {
                    title: projectTagline,
                    index: idx,
                    content: content,
                    lastUpdate: lastUpdate
                }
            });
        });
    });
});

まとめ

とまぁ、こんな感じにRendererのメソッドを書き換えることでやりたい放題できます。

なるべくMarkdown記法内で完結できるようにすると楽ですが、どうにもならなかったら独自のタグを作ってRenderer.htmlで無理矢理変換してしまうのがオススメです。何も考えなくて済むので。

かと言って独自タグだらけにするとただのHTMLになるんですが…。

参考