HTMLの組み立てかた (Shakespeare Hamlet)

(2017.4) 新規公開.

HTMLテンプレート Hamlet について.

Text.Htmlモジュール

HTMLを組み立てる方法としては, Text.Html モジュール ('html' パッケージ) がある。しかし、これは筋が悪すぎる。

  • タグ名が大文字で出力される. この時点で駄目。
  • タグの数だけ関数がある。新たに標準化されていくタグに対応できない。

これじゃない感が満載。

Hamlet

HTMLテンプレートの Hamlet を使おう。HTMLに特化したテンプレートで、字下げでHTML要素の入れ子関係を表す。

インストール

同名の 'hamlet' パッケージは非推奨 (deprecated)。'shakespeare' パッケージをダウンロードすること。

# cabal update
# cabal install --global --dry-run shakespeare

ただのHTML

まずは, 単なる文字列を作ってみる。Text.Hamlet モジュール.

準クォートとして, shamlet または hamlet を使う。単にテンプレートデータからHTMLを生成する場合は shamlet を使う。

Haskell
[POPUP]
  1. {-# LANGUAGE QuasiQuotes #-}
  2. import Data.Text.Lazy.IO as DTLI
  3. import Text.Blaze.Html.Renderer.Text -- Text バージョン. ByteString版もある.
  4. import Text.Hamlet
  5. main :: IO ()
  6. main = do
  7. DTLI.putStrLn $ renderHtml $ [shamlet|
  8. <ul>
  9. <li>はろー<em>わー</em>るど!!
  10. <a href="#x">リンク
  11. <li>fuga
  12. |]

実行結果::

$ ./hamlet-1
<ul><li>はろー<em>わー</em>るど!!<a href="#x">リンク</a>
</li>
<li>fuga</li>
</ul>

次に注目.

  1. 終了タグが補われている。
  2. 字下げで入れ子構造を表す
  3. 行の途中に挿入したタグはそのまま出力される。上の例ではemタグ

終了タグを補ってくれるのは善し悪しで、既存のHTMLデータを流用するときに逆に終了タグを消してやらないといけないし、このテンプレートデータはHTMLエディタで編集しづらい。

式の埋め込み

Hamlet はテンプレート内に式を埋め込める。

単に式を埋め込む場合は準クォート shamletを使い、それに加えて URL を式展開する場合は準クォート hamletを使う。

shamlet

まずは shamletの例.

  • #{...} 式の値を埋め込む。文字列中の <などはエスケープされる。値の型は Text, Intなど. 正確には ToMarkup型クラスを実装するデータ型。
  • ^{...} 別のテンプレートの結果を埋め込む。再エスケープされない。値の型は Html でなければならない。つまり単に Text はエラーになる。
Haskell
[POPUP]
  1. {-# LANGUAGE QuasiQuotes #-}
  2. {-# LANGUAGE OverloadedStrings #-}
  3. import Data.Text
  4. import Data.Text.Lazy.IO as DTLI
  5. import Text.Blaze.Html.Renderer.Text -- renderHtml :: Html -> Text
  6. import Text.Hamlet
  7. footer :: Html
  8. footer = [shamlet|
  9. <footer>
  10. <a href="/">Home
  11. |]
  12. -- #{...} はHTMLエスケープされる. エスケープしないときは ^{...}
  13. -- タグ内の #name は id属性に, .name はclass属性に変換される
  14. main :: IO ()
  15. main = do
  16. let var = "<わーるど>"::Text
  17. DTLI.putStr $ renderHtml $ [shamlet|
  18. <body>
  19. <div #block>hello #{var}
  20. <a href="/pages" .link .btn>ページの一覧
  21. <div>
  22. <a href="/pages/hoge/1">link to page
  23. ^{footer}
  24. |]

実行結果:

HTML
[POPUP]
  1. <body><div id="block">hello &lt;わーるど&gt;<a class="link btn" href="/pages">ページの一覧</a>
  2. </div>
  3. <div> <a href="/pages/hoge/1">link to page</a>
  4. </div>
  5. <footer><a href="/">Home</a>
  6. </footer>
  7. </body>

タグ内の #idname は id 属性に, .classname は class 属性になる。複数指定した場合も、きちんと空白区切りにまとめられる. good.

Type-safe URLs

さらに, URL展開については, 次の記号を使う。この場合, 準クォートは shamlet ではなく hamlet となる。

  • @{...} 列挙型を与える
  • @?{...} 列挙型に [(Text, Text)] 型の引数を与える.

hamlet準クォートは, テンプレートデータに加えて, URL文字列化関数を与える。

Haskell
[POPUP]
  1. {-# LANGUAGE QuasiQuotes #-}
  2. {-# LANGUAGE OverloadedStrings #-}
  3. import Data.Text
  4. import Data.Text.Lazy.IO as DTLI
  5. import Text.Blaze.Html.Renderer.Text -- renderHtml :: Html -> Text
  6. import Text.Hamlet
  7. -- URLのrouteは列挙型のデータ型で作る。
  8. -- 引数を取ったり取らなかったりできる。
  9. data MyRoute = MyHomeUrl | MyPageUrl Text
  10. -- URLの文字列を作る
  11. -- Render型は次のようになっている;
  12. -- type Render url = url -> [(Text, Text)] -> Text
  13. -- なので, 次でも同じ;
  14. -- myUrlRender :: MyRoute -> [(Text, Text)] -> Text
  15. myUrlRender :: Render MyRoute
  16. -- 第2引数は, テンプレート内で @?{...} で書いたときに与えることができる.
  17. -- Hamlet は URL文字列の空白や記号の %エンコードは行わない。
  18. myUrlRender MyHomeUrl _ = "/home?x=1&y=2 <\"<"
  19. myUrlRender (MyPageUrl x) [] = Data.Text.concat ["/pages/", x]
  20. myUrlRender (MyPageUrl x) (item:_) =
  21. Data.Text.concat ["/pages/", x, "/", snd item]
  22. -- URLの展開は @{...}. HTMLエスケープされる.
  23. -- type HtmlUrl url = Render url -> Html
  24. -- hamlet 準クォートから呼び出されたときは, 呼び出されたほうも引数を一つ取る.
  25. footer :: HtmlUrl MyRoute
  26. footer = [hamlet|
  27. <footer>
  28. <a href=@{MyHomeUrl}>Home
  29. |]
  30. -- hamlet 準クォートは, テンプレートと URL文字列化関数を取る.
  31. -- #{...} はHTMLエスケープされる. エスケープしないときは ^{...}
  32. -- URL展開は @{...} か @?{...}
  33. main :: IO ()
  34. main = do
  35. let var = "<わーるど>"::Text
  36. DTLI.putStr $ renderHtml $ [hamlet|
  37. <body>
  38. <div>hello #{var}
  39. <a href=@{MyPageUrl "hoge"}>hoge
  40. <div>
  41. <a href=@?{(MyPageUrl "hoge", [("page", "1")])}>link to page
  42. ^{footer}
  43. |] myUrlRender

実行結果:

HTML
[POPUP]
  1. <body><div>hello &lt;わーるど&gt;<a href="/pages/hoge">hoge</a>
  2. </div>
  3. <div> <a href="/pages/hoge/1">link to page</a>
  4. </div>
  5. <footer><a href="/home?x=1&amp;y=2 &lt;&quot;&lt;">Home</a>
  6. </footer>
  7. </body>

@{...} 内では, 文字列ではなく, 列挙型の値を指定する。これで、打ち間違いはコンパイルエラーになる。また、関数で, 複雑なURL文字列を構築できる。

コメントにも書いているが, URLの% エンコードは, Hamletの範囲外。URL文字列を構築する際に, 次のいずれかでエスケープすること。(後者の方が新しい.)

  • HTTPパッケージ, Network.HTTP モジュールの urlEncode :: String -> String
  • http-types パッケージ, Network.HTTP.Types モジュールの urlEncode :: Bool -> ByteString -> ByteString

制御構造

さらに, Hamlet テンプレートには制御構造を埋め込める。

テンプレートシステムは、次のいずれか;

  1. テンプレートに特別なタグや属性を埋め込んでおき, テンプレートの外からハッシュなどを与えて, 置換やループを行う方法。 => HTMLエディタがそのまま使える。値を与えるのにハッシュなどを作るのが間怠っこしい。
  2. テンプレート内に特別な構文を埋め込む => 外側のプログラムと書き方が違う。できることを増やすとどんどん黒魔術になる。
  3. 単にプログラムを埋め込めるようにする. RubyのERBや, 埋め込みではないがPHP. => ついビューの中でロジックを書いてしまい、見通しが悪くなりやすい。

Hamlet は, 2. と 3. の間ぐらい。制御構造の構文は独自のものを埋め込むが、判定式としてHaskellの式がそのまま書ける。

  • $if$elseif$else
  • $maybe, $nothing
  • $forall
  • $case$of

字下げは $... の位置で決まる.

Haskell
[POPUP]
  1. {-# LANGUAGE QuasiQuotes #-}
  2. {-# LANGUAGE OverloadedStrings #-}
  3. import Data.Text
  4. import Data.Text.Lazy.IO as DTLI
  5. import Text.Blaze.Html.Renderer.Text
  6. import Text.Hamlet
  7. isAdmin :: Text -> Bool
  8. isAdmin name = if name == "admin" then True else False
  9. isLoggedIn :: Bool
  10. isLoggedIn = True
  11. mylist :: [(Text, Text)]
  12. mylist = [("foo", "ふー"), ("bar", "ばー")]
  13. data Person = Person { personName :: Text }
  14. people :: [Person]
  15. people = [Person "n1", Person "n2", Person "n3"]
  16. -- people = []
  17. foo :: Either Text Text
  18. foo = Left "barrrrr"
  19. -- $... も字下げしないと, 前のHTML要素が終わってしまう.
  20. -- $... の字下げでどの要素の子になるか決まる.
  21. -- 例えば $case foo のところの<p>は, <ul>の子ではなく, <body>の子になる。
  22. main :: IO ()
  23. main = do
  24. DTLI.putStr $ renderHtml $ [shamlet|
  25. <body>
  26. $if isAdmin "username"
  27. <p>Welcome to the admin section.
  28. $elseif isLoggedIn
  29. <p>一般ユーザ
  30. $else
  31. <p>Please log in.
  32. $maybe name <- lookup "foo" mylist
  33. <p>Your name is #{name}
  34. $nothing
  35. <p>ゲスト
  36. $if Prelude.null people
  37. <p>No people.
  38. $else
  39. <ul>
  40. $forall person <- people
  41. <li>#{personName person}
  42. $case foo
  43. $of Left bar
  44. <p>It was left: #{bar}
  45. $of Right baz
  46. <p>It was right: #{baz}
  47. |]

実行結果:

HTML
[POPUP]
  1. <body><p>一般ユーザ</p>
  2. <p>Your name is ふー</p>
  3. <ul><li>n1</li>
  4. <li>n2</li>
  5. <li>n3</li>
  6. </ul>
  7. <p>It was left: barrrrr</p>
  8. </body>