Skip to content

Latest commit

 

History

History
725 lines (555 loc) · 27.5 KB

ch04.md

File metadata and controls

725 lines (555 loc) · 27.5 KB

Shakespearean 模板

标签(空格分隔): Yesod-Book


Shakespearean 模板

Yesod 使用Shakespearean系列模板语言作为其创建HTML,CSS,JavaScript的标准途径。这种系列语言使用相同的语法,以及首要原则:

  • 尽量少涉及底层语言,同时在不显眼的地方提供便利。
  • 编译时保证正确的内容格式
  • 静态类型安全性,很大程度上防止了XSS(Cross-site scripting)攻击。
  • 插入链接,只要有可能,通过类型安全的URL自动验证。

这些模板语言与Yesod之间没有本质上的关联,它们都可以独立出来。本章先介绍这些模板语言本身。后面再将其用在Yesod应用开发中。

概要

这里有四种主要语言:Hamlet是HTML模板语言,Julius是JavaScript模板语言,Cassius和Lucius都是CSS模板语言。Hamlet和Cassius都是空格敏感格式的,使用缩进来表明嵌套。而Lucius是CSS的超集,保持CSS的括号表示嵌套。Julius是一个产生JavaScript简单的直通语言;唯一添加的特性是可变的插值。

Cassius 实际上只是Lucius的一个替代语法。它们使用同样的处理引擎,不同的是Cassius文件在处理之前会把缩进zhuan转换成括号。这两种之间纯粹就是语法爱好之间的选择。

Hamlet(HTML)

$doctype 5
<html>
    <head>
        <title>#{pageTitle} - My Site
        <link rel=stylesheet href=@{Stylesheet}>
    <body>
        <h1 .page-title>#{pageTitle}
        <p>Here is a list of your friends:
        $if null friends
            <p>Sorry, I lied, you don't have any friends.
        $else
            <ul>
                $forall Friend name age <- friends
                    <li>#{name} (#{age} years old)
        <footer>^{copyright}

Lucius (CSS)

section.blog {
    padding: 1em;
    border: 1px solid #000;
    h1 {
        color: #{headingColor};
        background-image: url(@{MyBackgroundR});
    }
}

Cassius (CSS)

这个效果等同于上面

section.blog
    padding: 1em
    border: 1px solid #000
    h1
        color: #{headingColor}
        background-image: url(@{MyBackgroundR})

Julius (Javascript)

$(function(){
    $("section.#{sectionClass}").hide();
    $("#mybutton").click(function(){document.location = "@{SomeRouteR}";});
    ^{addBling}
});

类型

在介绍语法之前呢,我们先看一下使用的各种类型。我们在开头介绍中提到过,类型帮助我们免受XSS攻击。比如,我们有个HTML模板,它的功能是显示某个人的名字:

<p>Hello, my name is #{name}

#{...} 展示了我们怎样插入一个变量。

name 发生了怎样的变化?它的类型应该是什么呢?一个想当然的方法是使用Text值,逐字插入。但是这样会产生一个问题,如果name是这样的格式了怎么办?

<script src='http://nefarious.com/evil.js'></script>

我们需要将其编码,使<变成&lt

另外一个同样天真的方式是简单的转义文本的每一个字节。但如果已经存在有其他进程产生的HTML了会发生什么事情呢?比如,在Yesod网页中,所有的代码片段可以通过一个着色函数运行,这个函数使用span标签包装词语。( all Haskell code snippets are run through a colorizing function that wraps up words in appropriate span tags)如果我们转义所有,代码片段将会变得不可读!

替代方案,我们使用一个Html类型。为了产生Html值,我们有两个可供使用的API:ToMarkup类型类提供一种转换StringTextHtml的方式,通过toHtml方法,自动地转义。这种方式符合我们需求。对于代码片段的例子,我们使用preEscapedToMarkup函数。

当我们在Hamlet中使用变量插值的时候(HTML Shakespeare 语言),它在内部自动的调用了toHtml函数。所以如果我们插入了一个String,它会被编码。但是如果你提供了一个Html值,它在内部并不会被更改。在代码片段例子中,我们可能插入一个像这样的值#{preEscapedToMarkup myHaskellHtml}

Html类型,和上面提到的函数一样,都在blaze-html包中提供。这允许Hamlet与其他任何blaze-html包进行交互,然后需要Hamlet提供给一个通用的结局方案来产生blaze-html值。我们也将用于blaze-html惊人的性能。

相似的,我们有Css/ToCss,和 Javascript/ToJavascript。它们提供一些编译时检查,保证我们不会犯一些低级错误,比如在CSS中粘贴一些HTML。

在CSS方面,对于颜色和单位,还有一些辅助的数据类型。

.red { color: #{colorRed} }

请看 Haddock 文档了解更多细节。

类型安全的URL

可能在Yesod中,最为独特的特性就是类型安全的URL了,它们由Shakespeare提供,使用起来非常方便。用法和变量插值很接近;我们只是使用@符号替代#号。我们在后边讲解语法。

假定我们有一个应用,它有两个路由:*http://example.com/profile/home*是主页,*http://example.com/display/time*显示当前时间。我们想要从主页链接到时间页。我们可以思考以下三种构造URL方式:

  1. 相对路径:../display/time
  2. 绝对路径,不带域名:/display/time
  3. 绝对路径,带域名:http://example.com/display/time

每种途径都是有问题的: 第一种,当任一URL改变的时候,链接将不起作用,而且,它并不适用于所有情况,比如RSS和Atom反馈,它们需要绝对路径。 第二种,比第一种更有弹性,但是仍然不适用于RSS和Atom。 第三种,适用于所有情况,但是,如果你任何时候更改了域名,你都要更新应用中每个URL。你可能认为这种情况不长发生。想想你将应用从开发环境迁移到生产环境中的时候。

更重要的是,三种方式都由着巨大的缺陷:如果你更改了你的路由,编译器并不会警告你,你的链接出了问题。而且这里还没说错别字也可能造成巨大的问题。

类型安全的URL的目标是让编译器尽可能的帮助我们检查错误。为了促成这个目标,我们第一步需要定义一些数据类型,将其从纯粹的文本中移出来,因为编译器并不理解这些文本的含义。对于我们简单的应用,我们定义我们的路由类型:

data MyRoute = Home | Time

不要在我们的模板中使用如同* /display/time*的链接,我们使用Time构造器代替。但是最后,HTML还是由文本组成,而不是数据类型,所以我们需要将这些值转换成文本的方法。我们将其成为URL渲染方法,如下是个简单的例子:

renderMyRoute :: MyRoute -> Text
renderMyRoute Home = "http://example.com/profile/home"
renderMyRoute Time = "http://example.com/display/time"

URL 渲染函数可能要比这个稍微复杂一些。它们需要解决查询字符串参数,构造器中处理的记录,更智能地处理域名。但是实际上,你根本不需要担心这个问题,因为Yesod会自动创建你的渲染函数。类型签名有一些复杂来处理查询字符串:

type Query = [(Text, Text)]
type Render url = url -> Query -> Text
renderMyRoute :: Render MyRoute
renderMyRoute Home _ = ...
renderMyRoute Time _ = ...

好了,我们有了渲染函数,将类型安全的URL嵌入模板中。它们是怎样结合在一起的呢?不直接生成一个Html(或者CssJavascript)值,Shakespeare模板会生成一个函数,这个函数会使用这个渲染函数生成HTML。为了更直观,我们快速的看一眼Hamlet的工作机制。假定我们有一个模板:

<a href=@{Time}>The time

它会被翻译成Haskell代码:

\render -> mconcat ["<a href='", render Time, "'>The time</a>"]

语法

所有的Shakespeare语言都使用相同的插值语法,都能利用类型安全的URL。在特定的目标语言(HTML,CSS,Javascript)中,它们语法有区别。我们逐个探索它们。

Hamlet 语法

Hamlet是最为复杂的语言。不仅仅是因为它提供生成HTML的语法,而且还允许基本控制结构:条件,循环和Maybe

标签

很明显,标签是HTML模板中很重要的部分。在Hamlet中,我们尽量接近HTML语法,这样用起来更加舒服。但是,我们使用缩进来代替闭合标签。所以在HTML中我们这么写:

<body>
<p>Some paragraph.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</body>

在Hamlet中,我们要写成:

<body>
    <p>Some paragraph.
    <ul>
        <li>Item 1
        <li>Item 2

通常,你一旦习惯了这种方式,你会发现缩进方式更容易遵循。但是,当你处理标签前或者标签后的空白字符的时候,这就会变得非常棘手。如果你想创建一个如下形式的HTML

<p>Paragraph <i>italic</i> end.</p>

我们需要"Paragraph" 后边和"end" 前边有一个空白字符。为了实现这一点,我们需要两个转义字符:

<p>
    Paragraph #
    <i>italic
    \ end.

空白字符转义规则非常简单:

  1. 如果一行第一个非空白字符是反斜杠'',这个反斜杠会被忽略。(注意:这样仍然会导致,本行中所有标签都会被当成纯文本看待)
  2. 如果一行的最后一个字符是个'#',它会被忽略。

另外,Hamlet不会转义它内容的任何东西。这样已经存在的HTML就很容易拷贝进去。所以上面例子可以写成:

<p>Paragraph <i>italic</i> end.

注意,Hamlet会自动地为第一个标签生成闭合标签,但内部的"i"标签就不会自动生成。以上方式,你喜欢哪个就用哪个,都可以的。需要明白的是,只能在内部标签内使用闭合标签,正常的标签都是不闭合的。

另外,内部标签不能使用id和css类,举个例子:

<p #firstid>Paragraph <i #secondid>italic end.

生成HTML之后:

<p id="firstid">Paragraph <i #secondid>italic</i> end.</p>

注意p标签是自动闭合的,它的属性能够正常解析,i标签的属性被当成的纯文本对待了。

插值

对于简单的HTML,到目前为止看起来不错,但是现在还不能跟我们的Haskell代码交互。我们该怎样传值呢?使用插值:

<head>
    <title>#{title}

#{}被称为变量插值。这种情况下,title变量会被使用。我在叙述一遍:在这些变量调用的时候,Hamlet自动的访问这些变量。我们不需要特别的传值进去。

你可以在插值中应用函数。你可以在插值中使用字符串和数字量。你可以使用模块。括号和'$'符号可以把语句组合在一起。最后,toHtml函数被应用于结果,这就意味着,所有ToHtml的实例都可以使用插值。举个例子:

-- Just ignore the quasiquote stuff for now, and that shamlet thing.
-- It will be explained later.
{-# LANGUAGE QuasiQuotes #-}
import Text.Hamlet (shamlet)
import Text.Blaze.Html.Renderer.String (renderHtml)
import Data.Char (toLower)
import Data.List (sort)

data Person = Person
    { name :: String
    , age  :: Int
    }

main :: IO ()
main = putStrLn $ renderHtml [shamlet|
<p>Hello, my name is #{name person} and I am #{show $ age person}.
<p>
    Let's do some funny stuff with my name: #
    <b>#{sort $ map toLower (name person)}
<p>Oh, and in 5 years I'll be #{show ((+) 5 (age person))} years old.
|]
  where
    person = Person "Michael" 26

我们吹捧那么多的类型安全URL呢?它们跟变量插值几乎一致,除了它使用'@'符号开头。另外,使用'^'符号可以嵌入另外一个相同类型的模板。例子演示如下:

{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE OverloadedStrings #-}
import Text.Hamlet (HtmlUrl, hamlet)
import Text.Blaze.Html.Renderer.String (renderHtml)
import Data.Text (Text)

data MyRoute = Home

render :: MyRoute -> [(Text, Text)] -> Text
render Home _ = "/home"

footer :: HtmlUrl MyRoute
footer = [hamlet|
<footer>
    Return to #
    <a href=@{Home}>Homepage
    .
|]

main :: IO ()
main = putStrLn $ renderHtml $ [hamlet|
<body>
    <p>This is my page.
    ^{footer}
|] render

另外,这里有一种URL插值的变种,它可以插入查询字符串参数。这很有用,比如创建分页的响应。添加一个问号@?{...}表示一个查询参数。这个值必须是一个二维元组,其中第一个值必须是类型安全的URL,第二个是一个查询参数对列表。以下代码片段:

{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE OverloadedStrings #-}
import Text.Hamlet (HtmlUrl, hamlet)
import Text.Blaze.Html.Renderer.String (renderHtml)
import Data.Text (Text, append, pack)
import Control.Arrow (second)
import Network.HTTP.Types (renderQueryText)
import Data.Text.Encoding (decodeUtf8)
import Blaze.ByteString.Builder (toByteString)

data MyRoute = SomePage

render :: MyRoute -> [(Text, Text)] -> Text
render SomePage params = "/home" `append`
    decodeUtf8 (toByteString $ renderQueryText True (map (second Just) params))

main :: IO ()
main = do
    let currPage = 2 :: Int
    putStrLn $ renderHtml $ [hamlet|
<p>
    You are currently on page #{currPage}.
    <a href=@?{(SomePage, [("page", pack $ show $ currPage - 1)])}>Previous
    <a href=@?{(SomePage, [("page", pack $ show $ currPage + 1)])}>Next
|] render

生成的目标HTML:

<p>You are currently on page 2.
<a href="/home?page=1">Previous</a>
<a href="/home?page=3">Next</a>
</p>

属性

在上个例子中,我们在a标签中使用了一个href属性。我们解释一下语法:

  • 你可以在属性值中使用插值
  • 对于一个属性,等号和值是可选的,就像在HTML中<input type=checkbox checked>是完全没问题的。
  • 这里有两个方便的属性:对于id,你可以使用'#'号,对于CSS类,使用'.',换句话,<p #paragraphid .class1 .class2>
  • 属性值得引号是可选的,如果你想嵌入空白字符,那么引号是必须的。
  • 你可以通过使用冒号有选择的添加属性。让一个复选框只有在变量isChecked是True的情况下才被选中,你可能会写<input type=checkbox :isChecked:checked>。使一个段落变成红色的,你可以使用<p :isRed:style="color:red">(使用CSS类也能正常工作<p :isCurrent:.current>
  • 使用*{...}语法可以把任意键值对变成插值。这些插值必须是一个元组或者一个元组列表、文本或字符串。举个例子:如果我们有一个变量attrs = [("foo", "bar")],我们就能将其插入。

条件

终于,你想在你的页面里面添加一些逻辑。Hamlet的目标是让逻辑尽量简洁,把繁重的任务交给Haskell。这样,我们的逻辑很基本,就是ifelseifelse

$if isAdmin
    <p>Welcome to the admin section.
$elseif isLoggedIn
    <p>You are not the administrator.
$else
    <p>I don't know who you are. Please log in so I can decide if you get access.

所有正常的插值的规则同样适用于条件句的内容。

Maybe

相似的,我们有一个指定的结构来处理Maybe值。我们可以使用ifisJustfromJust,但是这样更加方便,而且能够避免局部函数。

$maybe name <- maybeName
    <p>Your name is #{name}
$nothing
    <p>I don't know your name.

左边除了简单的符号,你可以使用一些其他更加复杂的值,比如构造器和元组。

$maybe Person firstName lastName <- maybePerson
    <p>Your name is #{firstName} #{lastName}

右侧跟插值的规则一致,允许变量,函数等等。

Forall

我们还可以遍历列表。

$if null people
    <p>No people.
$else
    <ul>
        $forall person <- people
            <li>#{person}

Case

模式匹配是Haskell的利器之一。总和类型(Sum types)让你能够清晰的模拟世界上的类型,case语法能够让你安全的匹配,如果你丢掉了一种情况,编译器会警告你。Hamlet也有同样的能力。

$case foo
    $of Left bar
        <p>It was left: #{bar}
    $of Right baz
        <p>It was right: #{baz}

With

完善一些,我们有了with。它在为一个长表达式声明一个别名的时候非常便捷。

$with foo <- some very (long ugly) expression that $ should only $ happen once
    <p>But I'm going to use #{foo} multiple times. #{foo}

Doctype

最后一点语法糖: doctype语句。我们已经支持了许多不同版本的doctype,即使我们建议使用$doctype 5,这样会生成<!DOCTYPE html>

$doctype 5
<html>
    <head>
        <title>Hamlet is Awesome
    <body>
        <p>All done.

这是一种非常老但是仍然被支持的语法:三个感叹号(!!!)。你可能在别的地方代码中,仍然可以看到这个。我们没有计划将其移除,但是通常来说$doctype 趋于易读。

Lucius 语法

Lucius 是Shakespeare中两种CSS模板语言之一。它是CSS的一个超集,将来会在此基础上添加更多特性。

  • 类似Hamlet, 我们支持变量和URL插值。
  • CSS块支持嵌套
  • 支持在模板中生命变量
  • 一个作为混入(mixin)的CSS集可以在多个声明中复用。

第二点:你想给article中的一些标签指定样式。在纯CSS中,你可能必须这样写:

article code { background-color: grey; }
article p { text-indent: 2em; }
article a { text-decoration: none; }

这种情况,每行都需要写出article,这样还是有些臃肿的。想象一下如果有十几个或者更多的情况。这样虽然并不是世界上最糟糕的事情,但还是有些烦人的。Lucius帮助你从中解脱出来:

article {
    code { background-color: grey; }
    p { text-indent: 2em; }
    a { text-decoration: none; }
    > h1 { color: green; }
}

使用 Lucius 变量能够避免重复。一个简单的例子,定义一个公用的颜色:

@textcolor: #ccc; /* just because we hate our users */
body { color: #{textcolor} }
a:link, a:visited { color: #{textcolor} }

Mixin 是Lucius比较新的特性。我们声明一个mixin来提供一个属性集,然后将mixin使用(^)插入到模板中。举个例子演示下我们怎样使用mixin来处理一个供应商前缀问题。

{-# LANGUAGE QuasiQuotes #-}
import Text.Lucius
import qualified Data.Text.Lazy.IO as TLIO

-- Dummy render function.
render = undefined

-- Our mixin, which provides a number of vendor prefixes for transitions.
transition val =
    [luciusMixin|
        -webkit-transition: #{val};
        -moz-transition: #{val};
        -ms-transition: #{val};
        -o-transition: #{val};
        transition: #{val};
    |]

-- Our actual Lucius template, which uses the mixin.
myCSS =
    [lucius|
        .some-class {
            ^{transition "all 4s ease"}
        }
    |]

main = TLIO.putStrLn $ renderCss $ myCSS render

Cassius 语法

除了Lucius,Cassius 是一个空白字符敏感的备选方案。就像摘要中提到的,它们使用相同的处理引擎,但Cassius 预处理时,将插入括号闭合子代码块和分号来结束本行。这就意味着你可以使用所有Lucius的特性,举个简单的例子:

#banner
    border: 1px solid #{bannerColor}
    background-image: url(@{BannerImageR})

Julius 语法

Julius 是我们讨论过最为简单的语言,实际上,它几乎可以认为它就是JavaScript。Julia 支持我们提到的三种格式的插值。除此之外对内容不做任何的转换。

调用 Shakespeare

问题来了:我该怎么使用这东西呢?这里有三种不同的方式在你的Haskell代码中调用Shakespeare:

Quasiquotes

Quasiquotes 支持在Haskell代码中嵌入任意内容,并在编译时,将其转换成Haskell代码。

外部文件

这种情况中,模板代码在一个独立的文件中,通过Template Haskell引用。

刷新模式

以上两种模式在有任何变动的情况下,都需要完全重新编译。在刷新模式中,你的模板在一个独立的文件中,通过Template Haskell中引用。但是在运行时,外部文件每次都是从零开始重新解析。

刷新模式不支持Hamlet,仅仅支持Cassius,Lucius和Julius。在Hamlet中有太多的复杂特性直接依赖于Haskell编译器,在运行时重新实现是不切合实际的。

前两种途径的任何一种都能应用于生产环境。他们都将整个模板嵌入到最后的可执行文件中,简化了发布而且提高了性能。quasiquoter的优点是简单:所有东西都在一个单独的文件中。对于一个简短的模板,会非常适用。然而,通常上,建议使用外部文件方式:

  • 逻辑和展示分开
  • 容易更换外部文件和调试一些简单的CPP宏,这意味着你可以保持快速发展的同时,仍能获得高性能

因为这些特殊的QuasiQuoters和Template Haskell 函数,你需要保证使能这些语言扩展并且使用正确的语法。例子如下:

Quasiquoter

{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
{-# LANGUAGE QuasiQuotes #-}
import Text.Hamlet (HtmlUrl, hamlet)
import Data.Text (Text)
import Text.Blaze.Html.Renderer.String (renderHtml)

data MyRoute = Home | Time | Stylesheet

render :: MyRoute -> [(Text, Text)] -> Text
render Home _ = "/home"
render Time _ = "/time"
render Stylesheet _ = "/style.css"

template :: Text -> HtmlUrl MyRoute
template title = [hamlet|
$doctype 5
<html>
    <head>
        <title>#{title}
        <link rel=stylesheet href=@{Stylesheet}>
    <body>
        <h1>#{title}
|]

main :: IO ()
main = putStrLn $ renderHtml $ template "My Title" render

Extenal file

{-# LANGUAGE OverloadedStrings #-} -- we're using Text below
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE CPP #-} -- to control production versus debug
import Text.Lucius (CssUrl, luciusFile, luciusFileReload, renderCss)
import Data.Text (Text)
import qualified Data.Text.Lazy.IO as TLIO

data MyRoute = Home | Time | Stylesheet

render :: MyRoute -> [(Text, Text)] -> Text
render Home _ = "/home"
render Time _ = "/time"
render Stylesheet _ = "/style.css"

template :: CssUrl MyRoute
#if PRODUCTION
template = $(luciusFile "template.lucius")
#else
template = $(luciusFileReload "template.lucius")
#endif

main :: IO ()
main = TLIO.putStrLn $ renderCss $ template render
-- @template.lucius
foo { bar: baz }

该函数的命名方案是非常一致的。

|Language |Quasiquoter| External file|Reload| |-|-|-| |Hamlet|hamlet|hamletFile|N/A| |Cassius|cassius|cassiusFile|cassiusFileReload| |Lucius|lucius|luciusFile|luciusFileReload| |Julius|julius|juliusFile|juliusFileReload|

备选Hamlet类型

到目前为止,我们已经看到怎样从Hamlet产生一个HtmlUrl值,也就是一段嵌入类型安全的URL的HTML。我们可以使用Hamlet生成三种其他值:纯HTML,带URL的HTML、国际化消息和组件。最后一个会在组件一章进行介绍。

生成不带URL的纯HTML,我们使用“simplified Hamlet”。这里有一些变化:

  • 我们使用一个不同的函数集,它们前缀带‘s’。所以quasiquoter是shamlet,外部文件函数是shamletFile。怎样发音还有争议。
  • 不允许URL插值,否则会产生一个编译时错误
  • 不允许嵌入任意的HtmlUrl值。这个规则是说,嵌入的值必须和模板本身的类型一致,所以必须是Html。这意味着,对于shamlet,嵌入可以完全取代常规的变量插值(用哈希)。

在Hamlet中处理国际化(i18n)有一些复杂。Hamlet通过一个消息数据类型支持i18n,在概念和实现上于类型安全的URL非常相似。作为一个激进的例子,我们需要一个应用,向你问好并且告诉你,你已经吃了多少个苹果。我们可以使用一个数据类型展示出这些消息。

data Msg = Hello | Apples Int

然后,我们将其转换成一些人类能够读懂的语言,所以我们定一些转化函数:

renderEnglish :: Msg -> Text
renderEnglish Hello = "Hello"
renderEnglish (Apples 0) = "You did not buy any apples."
renderEnglish (Apples 1) = "You bought 1 apple."
renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]

我们现在想要将这些Msg值直接插入到模板中。我们使用下划线插值。

$doctype 5
<html>
    <head>
        <title>i18n
    <body>
        <h1>_{Hello}
        <p>_{Apples count}

这种方式的模板需要一些方式将这些值转换成HTML。所以就想类型安全URL一样,我们传入一个渲染函数。为了表现这些,我们定义一个别名:

type Render url = url -> [(Text, Text)] -> Text
type Translate msg = msg -> Html
type HtmlUrlI18n msg url = Translate msg -> Render url -> Html

在此时,你可以传入renderEnglishrenderSpanish、或者renderKlingon到这个模板,然后它会产生很好的转换输出。完整版的程序是:

{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE OverloadedStrings #-}
import Data.Text (Text)
import qualified Data.Text as T
import Text.Hamlet (HtmlUrlI18n, ihamlet)
import Text.Blaze.Html (toHtml)
import Text.Blaze.Html.Renderer.String (renderHtml)

data MyRoute = Home | Time | Stylesheet

renderUrl :: MyRoute -> [(Text, Text)] -> Text
renderUrl Home _ = "/home"
renderUrl Time _ = "/time"
renderUrl Stylesheet _ = "/style.css"

data Msg = Hello | Apples Int

renderEnglish :: Msg -> Text
renderEnglish Hello = "Hello"
renderEnglish (Apples 0) = "You did not buy any apples."
renderEnglish (Apples 1) = "You bought 1 apple."
renderEnglish (Apples i) = T.concat ["You bought ", T.pack $ show i, " apples."]

template :: Int -> HtmlUrlI18n Msg MyRoute
template count = [ihamlet|
$doctype 5
<html>
    <head>
        <title>i18n
    <body>
        <h1>_{Hello}
        <p>_{Apples count}
|]

main :: IO ()
main = putStrLn $ renderHtml
     $ (template 5) (toHtml . renderEnglish) renderUrl

其他 Shakespeare

除了HTML,CSS和Javascript帮助,还有一些通用目的的Shakespeare 可用。shakespeare-text提供了一个简单的方式创建插值字符串,就像如Ruby和Python等脚本语言的习惯。这个包的作用不限于Yesod。

{-# LANGUAGE QuasiQuotes, OverloadedStrings #-}
import Text.Shakespeare.Text
import qualified Data.Text.Lazy.IO as TLIO
import Data.Text (Text)
import Control.Monad (forM_)

data Item = Item
    { itemName :: Text
    , itemQty :: Int
    }

items :: [Item]
items =
    [ Item "apples" 5
    , Item "bananas" 10
    ]

main :: IO ()
main = forM_ items $ \item -> TLIO.putStrLn
    [lt|You have #{show $ itemQty item} #{itemName item}.|]

这个例子的几个点:

  • 请注意,我们有涉及三个不同的文本数据类型(String,strict Text和lazy Text)。都可以同时使用。
  • 我们使用一个叫lt的quasiquoter,来生成lazy text,这里还有st,生成strict text。
  • 同样,这里有长一些的名字(ltextstext)。

通常建议

这里有一些来自Yesod社区的通用提示,关于如何获取大多数Shakespeare。

  • 对于实际网页,使用外部文件,对于库,不是很长的文本,使用quasiquoters也是没问题的。
  • Patrick Brisbin 加入了Vim 代码高亮,这个很有用。
  • 你不应该在已存在的标签后嵌入开始、结束标签。唯一的例外是一个长文本内的<i>或者<b>标签