ElmでHTMLパーサを作った。
せっかくなので、ライブラリ制作に着手してから公開するまでのプロセスを書いてみる。Elm 開発の雰囲気を伝えるのが目的なので、特定のトピックが知りたい方はQiitaへどうぞ。(コードが沢山あるけど試してないので動かないかも。あと、途中でテストライブラリをアップデートしたりして実際に踏んだプロセスと違うし、コードも所々違うんだけど、それは無視して最短・最適のパスを踏んだことにする。)
経緯
Excel(とか他の表計算ソフト)からクリップボードにコピーしてWebアプリに貼り付けようとしたところ、フォーマットがHTMLだったのでパースしてデータを取り出したかった。ここで問題発生。
ElmにHTMLパーサが無いだと。。
— Yosuke Torii / ジンジャー (@jinjor) August 30, 2016
別にJSでパースしてElm側に送り込んでもいいんだけど、それだとなんとなく負けた気がするのでHTMLパーサを書くことにした。
プロジェクトを作る
プロジェクト用のディレクトリと簡単なElmコードを用意する。
elm-html-parser/ - src/ - HtmlParser.elm
module HtmlParser exposing (..) parse : String -> () parse s = ()
まだ何も決まってないのでこれでいい。早速コンパイル。
$ elm-make src/HtmlParser.elm
初回コンパイル時にelm-package.json
やらelm-stuff
やら色々出来る。
テストを書く
elm-community/elm-testを使う。と言っても、実際にはこのパッケージを手動でインストールする必要はなく、代わりにそのランナーであるnpmパッケージの elm-test をインストールして使う。
$ sudo npm install -g elm-test $ elm-test init
elm-test init
すると、テストに必要なひな形を作ってくれる。
tests/ - .gitignore - elm-package.json - Main.elm - Tests.elm
Tests.elm を編集。
module Tests exposing (..) import Test exposing (..) import Expect import HtmlParser all : Test all = describe "HtmlParser" [ test "basic" (\_ -> Expect.equal () (HtmlParser.parse "")) ]
ラムダ式になっている(\_ ->
)のは、ランダム値テストのため。fuzz関数を使うと与えた範囲でランダムな値を生成できる(CIの時は再現性が欲しいので、seedを固定値で指定する)。今回は使っていない。
elm-test
コマンドでテスト実行。
$ elm-test
GitHubとTravisCIのための設定
elm-stuff documentation.json
elm-stuff フォルダはライブラリの置き場所なので、必ず.gitignoreに入れておく。
続いて Travis CI にもelm-test
を叩いてもらうように設定する。
language: node_js node_js: - "4.2" before_script: - npm install -g elm - npm install -g elm-test - elm-package install -y script: elm-test
README.md にバッジを設置。
# elm-html-parser [![Build Status](https://travis-ci.org/jinjor/elm-html-parser.svg)] (https://travis-ci.org/jinjor/elm-html-parser)
あとは Travis CI で該当リポジトリをテストするように設定する。これで、プッシュする度にテストが回る環境が整った。
LISENCEは特に理由がなければBSD3が適当。
パーサを実装する
$ elm-package install Bogdanp/elm-combine
Bogdanp/elm-combine はパーサコンビネータのライブラリ。別のもあるけどこれが一番速い。elm-packageは--save
とか書かなくてもデフォルトでelm-package.jsonに追記してくれる。
まずは、AST(抽象構文木)の定義。
type Node = Text String | Element String Attributes (List Node) | Comment String parse : String -> List Node parse s = [] -- TODO 実装する
適当にテストを書いて失敗させる。
testParse : String -> List Node -> (() -> Expectation) testParse s ast = _ -> Expect.equal ast (HtmlParser.parse s) all : Test all = describe "HtmlParser" [ test "basic" (testParse "1" [Text "1"]) , test "basic" (testParse "<a></a>" [Element "a" [] []]) ]
次に、テストが通るまで頑張って実装。
parse : String -> List Node parse s = case fst (Combine.parse node s) of Ok x -> [x] Err _ -> [] node : Parser Node node = element `or` text text : Parser Node text = (\s -> Text s) `map` regex "[^<]*" element : Parser Node element = (\name _ -> Element name [] []) `map` startTag `andMap` endTag tagName : Parser String tagName = regex "[a-z][a-z0-9\\-]*" startTag : Parser String startTag = between (string "<") (string ">") tagName endTag : Parser String endTag = between (string "</") (string ">") tagName
これでテストが通る。あとはテスト増やす⇨実装する、の繰り返し。elm-combineの作者に教えてもらったトリビアとしては、Char型をなるべく使わずにString型とregexを使うと速くなる。
ドキュメントを書く
ライブラリが完成したらすぐに公開したいところだけど、公開するすべての型と関数にドキュメントを書くまで公にできない。
{-| Parse HTML. `` `elm parse "text" == [ Text "text" ] `` ` -} parse : String -> List Node parse s = ...
見た目をプレビューするには、以下のコマンドを打って出てきたJSONファイルをここで読み込む。
$ elm-make --docs=documentation.json
上のサイトはちょっとバグってるが気にしない。
パッケージを公開する
elm-package.json をいい感じに書き直す。公開前に確認するのはだいたい以下。
"repository": "https://github.com/jinjor/elm-html-parser.git", "source-directories": [ "src" ], "exposed-modules": [ "HtmlParser", "HtmlParser.Util" ],
exposed-modules
以外のモジュールは公開されないので、もし内部でのみ使うモジュールがあればHtmlParser.Internal
のようにしておくと良い。こうするとテストのためだけに関数を公開できたりして便利。
Gitのタグをつけて公開(公開されたパッケージ)。リンクするURLは/latest
にしておかないと古いドキュメントを参照してしまうという罠があるので気をつける。
$ git add -A $ git commit -a -m "implement something" $ git tag -a 1.0.0 -m "first release" $ git push origin master $ git push origin --tags $ elm-package publish
パッケージの公開に関して詳しくはuehajさんの記事に良くまとまってます。
デモページ
GitHubリポジトリのdocs
フォルダを使って公開する。docsフォルダに色々突っ込んでもリポジトリの言語のバーには反映されないっぽくて助かる。生成されたJavaScriptを入れると真っ黄色になるので。
ElmにまともなEditorライブラリがないのでace.jsを使う。Elmの port 機能を使うと外界のJavaScriptと会話できるので、aceエディタの文字を送り込んでパースさせる。
<script src="./script.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.0/ace.js"></script> <script type="text/javascript"> var app = Elm.Demo.fullscreen(); setTimeout(function() { var editor = ace.edit('editor'); editor.setTheme("ace/theme/monokai"); editor.getSession().setMode("ace/mode/html"); app.ports.init.send(editor.getValue()); window.addEventListener('keydown', function(e) { // Ctrl + S if(e.ctrlKey && e.keyCode == 83) { e.preventDefault(); app.ports.parse.send(editor.getValue()); } }, true); }); </script>
バージョンアップして再公開
デモページで色んなHTMLを突っ込んでみたらたまに失敗してたので、パッチバージョンを当てることにした。
まず失敗するテストケースを追加。
+ , test "basic" (testParse """<input data-foo2="a">""" [Element "input" [("data-foo2", "a")] []])
実装を修正する。
- map String.toLower (regex "[a-zA-Z][a-zA-Z:\\-]*") + map String.toLower (regex "[a-zA-Z][a-zA-Z0-9:\\-]*")
次のコマンドを打つとバージョンを上げてくれて、elm-package.json も更新される。
$ elm-package bump
バージョンアップの種類(MAJOR/MINOR/PATCH)は型を見て勝手に判定してくれる。今回はAPIを破壊していないし機能追加もしていないので、PATCH。いわゆるセマンティックバージョニングというやつ。
この時点では公開されていないので、先ほどと同じステップで公開する。
$ git add -A $ git commit -a -m "fix something" $ git tag -a 1.0.1 -m "second release" $ git push origin master $ git push origin --tags $ elm-package publish
宣伝する
Slack とか Twitter とか elm-discuss を使う。Slackは初心者の質問も常に受け付けているので、詰まったら聞いてみると答えが得られたりする。
まとめ
Elmでテスト駆動開発しつつパッケージを公開するまでの流れを紹介してみた。そんなにハマりどころはないと思う。