1. Haskell‎ > ‎05. Yesod‎ > ‎

02. Shakespearean Templates


4つのテンプレート言語


Shakespearean Templatesとは、Webコンテンツを構成するテキストをHaskell/Yesodで生成する、下記のテンプレート言語群のことです。
  • Hamlet(HTML)
  • Julius(JavaScript)
  • Cassius(CSS)
  • Lucius(CSS)
これらは全てWebサーバに依存せず、汎用的に利用可能です。生成されたテキストを、コンソールアプリケーションで簡単に出力できます。




Hamlet


Hamletは、HTML生成用のテンプレート言語です。


とりあえずコンソールに出力してみる


{-# LANGUAGE QuasiQuotes  #-}
import Prelude hiding (putStr)
import Data.Text.Lazy.IO
import Text.Blaze.Html.Renderer.Text
import Text.Hamlet

main :: IO ()
main = putStr $ renderHtml $ [shamlet|
    <p>abc
    |]

実行結果
<p>abc</p>

まず、renderHtml関数ですが、Text.Blaze.Htmlにて定義されているHtml型のデータを、文字列を表す型に変換します。上記の場合はText.Blaze.Html.Renderer.Textなので、Text型に変換しています。
それを、putStr(Data.Text.Lazy.IO)で標準出力に送っています。ココまでは、Shakespearean Templatesとは関係ない部分です。

[shamlet| と |] の準クォートの部分が、Hamletによる部分です。shamletは、内部のテンプレートを展開して、Html型データを返します。
※shamletは、一番単純な準クォートです。後程、Type-safe URLsを扱えるhamletについても述べていきます。
テンプレートの内部は、一見すると終了タグの無い中途半端なHTMLのように見えますが、実行結果には終了タグがちゃんと現れています。

実は、テンプレートとして記述するのは、純粋なHTMLではありません。
Hamletの基本的な文法として、
<タグ>タグ内容
という感じで記述します。この結果として生成されるHTMLが
<タグ>タグ内容</タグ>
となるのです。テンプレート内でHTMLのように終了タグを記述すると、結果のHTMLには終了タグが2重で出力されることになります。


タグ


タグの親子関係は、インデントによって表します。

<body>
    <div>hello world
とすれば、
<body><div>hello world</div>
</body>
のように、子供にあたるタグがネストされて出力されます。

<body>
    <div>hello world
    <div>hello world, again
のように、インデントを揃えたタグは、同レベルとして出力されます。
<body><div>hello world</div>
<div>hello world, again</div>
</body>

テキスト行(タグが無い行)の場合も同様のルールが適用されます。インデントを下げた場合は、直前の親行のタグに内包されます。
<body>
    <div>hello
         world

<body><div>helloworld</div>
</body>
となります。

なお、Hamletのインデントにはタブ文字を使うことが出来ないので、上記のようなインデントを行うにはスペース文字を使ってください。

とは言え「タグをネストさせるためにいちいち改行してインデント付けるの面倒くさい」という方もいらっしゃるかと思います。
実は、Hamletは行頭に現れたタグ以外は、タグとして認識しないので、テンプレートで
<p>abc<b>def</b>
とすれば、出力結果は
<p>abc<b>def</b></p>
という風に、最初のタグに対してのみ終了タグが付加されます。
※一応、参考サイトでは「この方法でタグをネストさせるのは<i>や<b>みたいなインライン要素のみにしておいて、ブロックレベル要素はちゃんとインデントでやったほうがええよ」と言っています。

(インデントを除いて)最初の文字がバックスラッシュ(Windowsなら円記号)で始まる行は、タグがあってもタグとして認識されなくなるので、終了タグが付加されません。
<body>
    \<div>hello world

<body><div>hello world</body>
のように展開されます。エスケープ文字であるバックスラッシュは出力されません。


値の挿入


#{値}
で、HTML展開時に指定した値を挿入できます。

{-# LANGUAGE QuasiQuotes  #-}
import Prelude hiding (putStr)
import Data.Text.Lazy.IO
import Text.Blaze.Html.Renderer.Text
import Text.Hamlet

main :: IO ()
main = putStr $ renderHtml $ [shamlet|
<body>
   <div>hello #{var}
|]
    where
        var = "world"

関数だって呼び出せます。
{-# LANGUAGE OverloadedStrings, QuasiQuotes  #-}
import Prelude hiding (putStr)
import Data.Text.Lazy
import Data.Text.Lazy.IO
import Text.Blaze.Html.Renderer.Text
import Text.Hamlet

main :: IO ()
main = putStr $ renderHtml $ [shamlet|
<body>
   <div>hello #{func "world"}
|]
    where
        func arg = append arg ", again"

値の中に<や>等、エスケープが必要な文字があれば、自動的に変換されます。
{-# LANGUAGE QuasiQuotes  #-}
import Prelude hiding (putStr)
import Data.Text.Lazy.IO
import Text.Blaze.Html.Renderer.Text
import Text.Hamlet

main :: IO ()
main = putStr $ renderHtml $ [shamlet|
<body>
   <div>hello #{var}
|]
    where
        var = "<world>"

展開結果
<body><div>hello &lt;world&gt;</div>
</body>


Type-safe URLs


HamletではURL置換について、ユーザが「ルートデータ型」と呼ばれる、独自の列挙型を定義し、ルートデータ型の値からURLを生成するレンダラ関数を定義することで、値とURLを1対1に対応させます。
不要なURLを削除する際に、ルートデータ型の値を削除すれば、その値を使っているテンプレートはコンパイル時にエラーとなります。

{-# LANGUAGE OverloadedStrings, QuasiQuotes  #-}
import Prelude hiding (putStr)
import Data.Text.Lazy.IO
import Data.Text
import Text.Blaze.Html.Renderer.Text
import Text.Hamlet

data ExampleSiteRoute = Hello | World

render :: ExampleSiteRoute -> [(Text, Text)] -> Text
render Hello _ = "http://example.jp/hello.html"
render World _ = "http://example.jp/world.html"

main :: IO ()
main = putStr $ renderHtml $ [hamlet|
<div><a href="@{Hello}"</a>
<div><a href="@{World}"</a>
|] render

展開結果
<div><a href="http://example.jp/hello.html"</a></div>
<div><a href="http://example.jp/world.html"</a></div>

上記の例では、URLに対応するルートデータ型ExampleSiteRouteを定義しています。値はHelloとWorldの2値です。

続けて、ExampleSiteRouteの値から、URLを生成するレンダラ関数renderを定義しています。
レンダラ関数の第1引数はルートデータ型ですが、第2引数は(キー、値)のタプルの配列と決まっています。これは、テンプレート側からパラメータを渡すためのものです。これを用いてクエリ文字列等を作成します。
なお、上記の例では、クエリ文字列等が不要なシンプルなURLを作成するので、第2引数はアンダースコアで捨てています。

準クォートは、これまでのshamletではなく、hamletを用います。hamletは、Type-safe URLsを扱うために、準クォートの後にレンダラ関数を受け取ります。
テンプレート内部では、URLに置換する部分を、@{ルートデータ型の値}で表します。指定した値でレンダラ関数の動作を決め、対応するURLに置換させます。

なお、パラメータを渡す場合、@{}ではなく@?{}を用います。
@{}の中身は(URLに対応するルートデータ型の値, (キー、値)のタプルの配列)のタプルです。第2要素のタプル配列が、レンダラ関数の第2引数に渡されます。
{-# LANGUAGE OverloadedStrings, QuasiQuotes  #-}
import Prelude hiding (putStr)
import Data.Text.Lazy.IO
import Data.Text
import Text.Blaze.Html.Renderer.Text
import Text.Hamlet

data ExampleSiteRoute = Hello | World

render :: ExampleSiteRoute -> [(Text, Text)] -> Text
render Hello dic = "http://example.jp/hello.html?" `append` makeQueryString dic
render World _ = "http://example.jp/world.html"

makeQueryString :: [(Text, Text)] -> Text
makeQueryString dic = intercalate "&" (Prelude.map (\(key, value) -> key `append` "=" `append` value) dic)

main :: IO ()
main = putStr $ renderHtml $ [hamlet|
<div><a href="@?{(Hello, [("id", "12345"), ("word", "foo")])}"</a>
<div><a href="@{World}"</a>
|] render

展開結果
<div><a href="http://example.jp/hello.html?id=12345&word=foo"</a></div>
<div><a href="http://example.jp/world.html"</a></div>


テンプレートの外部ファイル化

通常は、テンプレートは外部ファイルにしておきます。

下記の例では、テンプレートをexample.shamletファイルとして、shamletFileでそれを読み込んでいます。
shamletFileは、shamlet準クォートに対応しています。
なお、shamletFileはTemplate Haskell関数のため、$()で囲む必要があります。

example.shamlet
<body>
    <div>hello #{func "world"}

Example.hs
{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}
import Prelude hiding (putStr)
import Data.Text.Lazy.IO
import Data.Text
import Text.Blaze.Html.Renderer.Text
import Text.Hamlet

func :: Text-> Text
func arg = append arg ", again"

template :: Html
template = $(shamletFile "example.shamlet")

main :: IO ()
main = putStr $ renderHtml $ template

hamlet準クォートに対応する外部ファイル読み込み関数は、hamletFileです。

example.hamlet
<div><a href="@?{(Hello, [("id", "12345"), ("word", "foo")])}"</a>
<div><a href="@{World}"</a>

Example.hs
{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}
import Prelude hiding (putStr)
import Data.Text.Lazy.IO
import Data.Text
import Text.Blaze.Html.Renderer.Text
import Text.Hamlet

data ExampleSiteRoute = Hello | World

render :: ExampleSiteRoute -> [(Text, Text)] -> Text
render Hello dic = "http://example.jp/hello.html?" `append` makeQueryString dic
render World _ = "http://example.jp/world.html"

makeQueryString :: [(Text, Text)] -> Text
makeQueryString dic = intercalate "&" (Prelude.map (\(key, value) -> key `append` "=" `append` value) dic)

template :: HtmlUrl ExampleSiteRoute
template = $(hamletFile "example.hamlet")

main :: IO ()
main = putStr $ renderHtml $ template render

※レンダラ関数のText型は、外部ファイルの場合はData.Textでないとコンパイルエラーとなります。準クォートの場合はData.Text.Lazyでも大丈夫ですが。


改行あれこれ


※イロイロ書いてはみましたが、結論から言えば、展開されるHTMLの改行にはこだわらないのが一番です…。


基本


先程の例で
<body>
    <div>hello world

<body><div>hello world</div>
</body>
に展開されると述べました。

デフォルトの、HTML展開時の改行ルールは以下の通りです。
  • インデントのネストが進んでいく間は、同じ行に展開します。
  • ネストが進まなければ、改行されます。
上記の場合、
  • <div>の行はインデントのネストが深くなっているので、直前の<body>と同じ行に展開されます。
  • <div>の行でネストが終わっているので、改行されます。

複雑な例として
<body>
    <div>ab
        <div>cd
            <div>ef
        <div>gh
        <div>ij
    <div>kl
        <div>mn
は、下記のように展開されます。
<body><div>ab<div>cd<div>ef</div>
</div>
<div>gh</div>
<div>ij</div>
</div>
<div>kl<div>mn</div>
</div>
</body>
インデントのネストが進む間は同じ行になり、ネストが進まなければ改行されます。


#エスケープ


ネストが進まなくても改行をさせたくない場合は、#を用います。
<body>
    <div>hello world#
は、下記のように展開されます。
<body><div>hello world</div></body>
</div>の後の改行がされなくなっています。エスケープ文字である#は出力されません。


テキストを改行


タグ内容のテキストを改行させたい場合はどうしたら良いでしょう?

例えば
<body>
    <div>hello
         world

<body><div>helloworld</div>
</body>
となり、上手くいきません。タグのあるなしに関係なく、先の改行ルールが適用されるためです。"world"の行はネストが進んでいるので、<div>の行と同じ行に展開されます。

改行ルールに従って、下記のように記述すれば、タグの内容を改行できます。改行させたい行を同じインデントにすれば、ネストが進まないので改行されるわけです。
<body>
    <div>
        hello
        world
は、下記のように展開されます。
<body><div>hello
world</div>
</body>

なお、インデントを揃えるからといって、
<body>
    <div>hello
    world
のようにしてしまうと、下記のように展開されます。
<body><div>hello</div>
world</body>
<div>の行でネストが進まなくなるので、</div>と閉じられてしまいます。


手動で改行


どうしても改行したい部分がある場合は、改行位置にバックスラッシュのみの行を記述します。

例えば、開始タグの直後で改行させた場合は
<body>
    \
    <div>hello world

とすれば、下記のように展開されます。
<body>
<div>hello world</div>
</body>


$newlineステートメント


コレまでのデフォルトの動作を変更するために、$newlineステートメントが用意されています。例えば
main = putStr $ renderHtml $ [shamlet|$newline never
<body>
    <div>hello world
|]
とすれば
<body><div>hello world</div></body>
と、全く改行されなくなります。

$newlineステートメントには、下記の3つの値が指定できます。
 always デフォルトの改行ルールです
 text テキストでは改行されますが、タグでは改行されません
 never 自動改行はされません

textの動きについて説明します。下記のような場合
main = putStr $ renderHtml $ [shamlet|$newline text
<body>
    <div>hello world
    <div>
        hello world,
        again
|]
下記のように展開されます。
<body><div>hello world</div><div>hello world,
again</div></body>
タグが無い行についてはルールに従い改行されますが、タグについては改行されません。

neverが指定されている場合も、バックスラッシュのみの行による、手動改行は有効です。




Julius





*Cassius





*Lucius





参考


Shakespearean Templates - Yesod Web Framework Book
https://sites.google.com/site/klovelab/



Comments