ジンジャー研究室

長めのつぶやき。難しいことは書きません。

ElmでHTMLパーサを作って公開するまでの手順

ElmでHTMLパーサを作った。

github.com

せっかくなので、ライブラリ制作に着手してから公開するまでのプロセスを書いてみる。Elm 開発の雰囲気を伝えるのが目的なので、特定のトピックが知りたい方はQiitaへどうぞ。(コードが沢山あるけど試してないので動かないかも。あと、途中でテストライブラリをアップデートしたりして実際に踏んだプロセスと違うし、コードも所々違うんだけど、それは無視して最短・最適のパスを踏んだことにする。)

経緯

Excel(とか他の表計算ソフト)からクリップボードにコピーしてWebアプリに貼り付けようとしたところ、フォーマットがHTMLだったのでパースしてデータを取り出したかった。ここで問題発生。

別にJSでパースしてElm側に送り込んでもいいんだけど、それだとなんとなく負けた気がするのでHTMLパーサを書くことにした。

プロジェクトを作る

プロジェクト用のディレクトリと簡単なElmコードを用意する。

elm-html-parser/
  - src/
    - HtmlParser.elm
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 を編集。

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のための設定

.gitignore
elm-stuff
documentation.json

elm-stuff フォルダはライブラリの置き場所なので、必ず.gitignoreに入れておく。

続いて Travis CI にもelm-testを叩いてもらうように設定する。

.travis.yml
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 にバッジを設置。

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(抽象構文木)の定義。

src/HtmlParser.elm
type Node
  = Text String
  | Element String Attributes (List Node)
  | Comment String


parse : String -> List Node
parse s = [] -- TODO 実装する

適当にテストを書いて失敗させる。

tests/Tests.elm
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" [] []])
    ]

次に、テストが通るまで頑張って実装。

src/HtmlParser.elm
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を使うと速くなる。

ドキュメントを書く

ライブラリが完成したらすぐに公開したいところだけど、公開するすべての型と関数にドキュメントを書くまで公にできない。

src/HtmlParser.elm
{-| Parse HTML.

`` `elm
parse "text" == [ Text "text" ]
`` `
-}
parse : String -> List Node
parse s = ...

見た目をプレビューするには、以下のコマンドを打って出てきたJSONファイルをここで読み込む。

$ elm-make --docs=documentation.json

上のサイトはちょっとバグってるが気にしない。

パッケージを公開する

elm-package.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を突っ込んでみたらたまに失敗してたので、パッチバージョンを当てることにした。

まず失敗するテストケースを追加。

tests/Tests.elm
+    , test "basic" (testParse """<input data-foo2="a">""" [Element "input" [("data-foo2", "a")] []])

実装を修正する。

src/HtmlParser.elm
-  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でテスト駆動開発しつつパッケージを公開するまでの流れを紹介してみた。そんなにハマりどころはないと思う。