(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
-
-
- import Data.Text.Lazy.IO as DTLI
- import Text.Blaze.Html.Renderer.Text
- import Text.Hamlet
-
- main :: IO ()
- main = do
- DTLI.putStrLn $ renderHtml $ [shamlet|
- <ul>
- <li>はろー<em>わー</em>るど!!
- <a href="#x">リンク
- <li>fuga
- |]
実行結果::
$ ./hamlet-1
<ul><li>はろー<em>わー</em>るど!!<a href="#x">リンク</a>
</li>
<li>fuga</li>
</ul>
次に注目.
- 終了タグが補われている。
- 字下げで入れ子構造を表す
- 行の途中に挿入したタグはそのまま出力される。上の例ではemタグ
終了タグを補ってくれるのは善し悪しで、既存のHTMLデータを流用するときに逆に終了タグを消してやらないといけないし、このテンプレートデータはHTMLエディタで編集しづらい。
式の埋め込み
Hamlet はテンプレート内に式を埋め込める。
単に式を埋め込む場合は準クォート shamlet
を使い、それに加えて URL を式展開する場合は準クォート hamlet
を使う。
shamlet
まずは shamlet
の例.
#{...}
式の値を埋め込む。文字列中の <などはエスケープされる。値の型は Text
, Int
など. 正確には ToMarkup
型クラスを実装するデータ型。
^{...}
別のテンプレートの結果を埋め込む。再エスケープされない。値の型は Html
でなければならない。つまり単に Text
はエラーになる。
Haskell
-
-
-
- import Data.Text
- import Data.Text.Lazy.IO as DTLI
- import Text.Blaze.Html.Renderer.Text
- import Text.Hamlet
-
- footer :: Html
- footer = [shamlet|
- <footer>
- <a href="/">Home
- |]
-
-
-
- main :: IO ()
- main = do
- let var = "<わーるど>"::Text
- DTLI.putStr $ renderHtml $ [shamlet|
- <body>
- <div #block>hello #{var}
- <a href="/pages" .link .btn>ページの一覧
- <div>
- <a href="/pages/hoge/1">link to page
- ^{footer}
- |]
実行結果:
HTML/XML
- <body><div id="block">hello <わーるど><a class="link btn" href="/pages">ページの一覧</a>
- </div>
- <div> <a href="/pages/hoge/1">link to page</a>
- </div>
- <footer><a href="/">Home</a>
- </footer>
- </body>
タグ内の #idname は id 属性に, .classname は class 属性になる。複数指定した場合も、きちんと空白区切りにまとめられる. good.
Type-safe URLs
さらに, URL展開については, 次の記号を使う。この場合, 準クォートは shamlet
ではなく hamlet
となる。
@{...}
列挙型を与える
@?{...}
列挙型に [(Text, Text)]
型の引数を与える.
hamlet
準クォートは, テンプレートデータに加えて, URL文字列化関数を与える。
Haskell
-
-
-
- import Data.Text
- import Data.Text.Lazy.IO as DTLI
- import Text.Blaze.Html.Renderer.Text
- import Text.Hamlet
-
-
-
- data MyRoute = MyHomeUrl | MyPageUrl Text
-
-
-
-
-
-
- myUrlRender :: Render MyRoute
-
-
-
- myUrlRender MyHomeUrl _ = "/home?x=1&y=2 <\"<"
- myUrlRender (MyPageUrl x) [] = Data.Text.concat ["/pages/", x]
- myUrlRender (MyPageUrl x) (item:_) =
- Data.Text.concat ["/pages/", x, "/", snd item]
-
-
-
-
- footer :: HtmlUrl MyRoute
- footer = [hamlet|
- <footer>
- <a href=@{MyHomeUrl}>Home
- |]
-
-
-
-
- main :: IO ()
- main = do
- let var = "<わーるど>"::Text
- DTLI.putStr $ renderHtml $ [hamlet|
- <body>
- <div>hello #{var}
- <a href=@{MyPageUrl "hoge"}>hoge
- <div>
- <a href=@?{(MyPageUrl "hoge", [("page", "1")])}>link to page
- ^{footer}
- |] myUrlRender
実行結果:
HTML/XML
- <body><div>hello <わーるど><a href="/pages/hoge">hoge</a>
- </div>
- <div> <a href="/pages/hoge/1">link to page</a>
- </div>
- <footer><a href="/home?x=1&y=2 <"<">Home</a>
- </footer>
- </body>
@{...}
内では, 文字列ではなく, 列挙型の値を指定する。これで、打ち間違いはコンパイルエラーになる。また、関数で, 複雑なURL文字列を構築できる。
コメントにも書いているが, URLの% エンコードは, Hamletの範囲外。URL文字列を構築する際に, 次のいずれかでエスケープすること。(後者の方が新しい.)
- HTTPパッケージ,
Network.HTTP
モジュールの
urlEncode :: String -> String
- http-types パッケージ,
Network.HTTP.Types
モジュールの
urlEncode :: Bool -> ByteString -> ByteString
制御構造
さらに, Hamlet テンプレートには制御構造を埋め込める。
テンプレートシステムは、次のいずれか;
- テンプレートに特別なタグや属性を埋め込んでおき, テンプレートの外からハッシュなどを与えて, 置換やループを行う方法。
=> HTMLエディタがそのまま使える。値を与えるのにハッシュなどを作るのが間怠っこしい。
- テンプレート内に特別な構文を埋め込む
=> 外側のプログラムと書き方が違う。できることを増やすとどんどん黒魔術になる。
- 単にプログラムを埋め込めるようにする. RubyのERBや, 埋め込みではないがPHP.
=> ついビューの中でロジックを書いてしまい、見通しが悪くなりやすい。
Hamlet は, 2. と 3. の間ぐらい。制御構造の構文は独自のものを埋め込むが、判定式としてHaskellの式がそのまま書ける。
$if
〜 $elseif
〜 $else
$maybe
, $nothing
$forall
$case
〜 $of
字下げは $... の位置で決まる.
Haskell
-
-
-
- import Data.Text
- import Data.Text.Lazy.IO as DTLI
- import Text.Blaze.Html.Renderer.Text
- import Text.Hamlet
-
- isAdmin :: Text -> Bool
- isAdmin name = if name == "admin" then True else False
-
- isLoggedIn :: Bool
- isLoggedIn = True
-
- mylist :: [(Text, Text)]
- mylist = [("foo", "ふー"), ("bar", "ばー")]
-
- data Person = Person { personName :: Text }
- people :: [Person]
- people = [Person "n1", Person "n2", Person "n3"]
-
-
- foo :: Either Text Text
- foo = Left "barrrrr"
-
-
-
-
- main :: IO ()
- main = do
- DTLI.putStr $ renderHtml $ [shamlet|
- <body>
- $if isAdmin "username"
- <p>Welcome to the admin section.
- $elseif isLoggedIn
- <p>一般ユーザ
- $else
- <p>Please log in.
-
- $maybe name <- lookup "foo" mylist
- <p>Your name is #{name}
- $nothing
- <p>ゲスト
-
- $if Prelude.null people
- <p>No people.
- $else
- <ul>
- $forall person <- people
- <li>#{personName person}
-
- $case foo
- $of Left bar
- <p>It was left: #{bar}
- $of Right baz
- <p>It was right: #{baz}
- |]
実行結果:
HTML/XML
- <body><p>一般ユーザ</p>
- <p>Your name is ふー</p>
- <ul><li>n1</li>
- <li>n2</li>
- <li>n3</li>
- </ul>
- <p>It was left: barrrrr</p>
- </body>