莎士比亚模板
Yesod使用莎士比亚系列模板语言作为HTML,CSS和Javascript创建的标准方法。这个语言系列有一些共同的语法,以及总体原则: 尽可能少地干扰底层语言,同时提供不显眼的便利。 编译时保证格式良好的内容。 静态类型安全,极大地帮助防止XSS(跨站点脚本)攻击。 尽可能通过类型安全的URL自动验证插值链接。 没有任何东西固有地将Yesod与这些语言捆绑在一起,或者相反:每种语言都可以独立使用。本章将单独介绍这些模板语言,本书的其余部分将使用它们来增强Yesod应用程序开发。
概要
主要有四种语言:Hamlet是一种HTML模板语言,Julius是Javascript,Cassius和Lucius都是CSS。 Hamlet和Cassius都是对空格敏感的格式,使用缩进来表示嵌套。相比之下,Lucius是CSS的超集,保留了CSS表示嵌套的大括号。 Julius是一种用于生成Javascript的简单直通语言;唯一增加的功能是变量插值。 Cassius实际上只是Lucius的另一种语法。它们都在下面使用相同的处理引擎,但Cassius文件在处理之前将缩进转换为大括号。两者之间的选择纯粹是语法偏好之一。
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(js)
$(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网站上,所有Haskell代码片段都通过一个着色函数运行,该函数将单词包装在适当的span标签中。 如果我们实体逃脱了一切,代码片段将是完全不可读的! 相反,我们有一个Html数据类型。 为了生成Html值,我们有两个API选项:ToMarkup类型类提供了一种通过其toHtml函数将String和Text值转换为Html的方法,并在此过程中自动转义实体。 这将是我们想要的上述名称的方法。 对于代码片段示例,我们将使用preEscapedToMarkup函数。 当您在Hamlet(HTML莎士比亚语言)中使用变量插值时,它会自动对内部的值应用toHtml调用。 因此,如果插入一个String,它将被实体转义。 但是,如果您提供Html值,它将显示为未修改。 在代码片段示例中,我们可能使用#{preEscapedToMarkup myHaskellHtml}之类的内容进行插值。
Html数据类型以及提到的函数都是由blaze-html包提供的。 这允许Hamlet与所有其他blaze-html包进行交互,并让Hamlet为生成blaze-html值提供通用解决方案。 此外,我们可以利用blaze-html的惊人表现。 同样,我们有Css / ToCss,以及Javascript / ToJavascript。 这些提供了一些编译时的健全性检查,我们没有意外地将一些HTML粘贴到我们的CSS中。
CSS方面的另一个优点是颜色和单位的一些辅助数据类型。 例如:
.red {color:#{colored}}
复制代码
类型安全的URL
Yesod中最独特的功能可能是类型安全的URL,并且莎士比亚直接提供了方便使用它们的能力。用法几乎与变量插值相同;我们只使用at符号(@)而不是哈希(#)。我们稍后将介绍语法;首先,让我们澄清一下直觉。 假设我们有一个包含两个路径的应用程序:http://example.com/profile/home
是主页,http://example.com/display/time
显示当前时间。让我们说我们想要从主页链接到时间。我可以想到三种不同的构建URL的方法:
- 作为相对链接:
../ display / time
- 作为绝对链接,没有域名:
/ display / time
- 作为绝对链接,使用域名:
http://example.com/display/time
每种方法都存在问题:如果任一URL更改,第一种方法都会中断。此外,它不适用于所有用例;例如,RSS和Atom提要需要绝对URL。第二种方式比第一种更具弹性,但仍然不能被RSS和Atom接受。虽然第三种方法适用于所有用例,但只要域名发生变化,您就需要更新应用程序中的每个URL。你认为这不经常发生吗?等到你从开发转移到升级和最终生产服务器。 但更重要的是,所有方法都存在一个巨大的问题:如果您根本改变路由,编译器将不会警告您链接断开。更不用说拼写错误也会造成严重破坏。 类型安全URL的目标是让编译器尽可能地为我们检查事物。为了促进这一点,我们的第一步必须是从编译器不理解的普通旧文本转移到一些定义良好的数据类型。对于我们的简单应用程序,让我们使用sum类型对路径建模:
data MyRoute = Home | Time
复制代码
我们可以使用Time构造函数,而不是在我们的模板中放置像/ display / 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(或Css或Javascript)值,莎士比亚模板实际上产生了一个函数,它接受这个渲染函数并生成HTML。 为了更好地看待这一点,让我们快速(假)看看哈姆雷特将如何在表面下工作。 假设我们有一个模板:
<a href=@{Time}>The time
复制代码
这将大致转换为Haskell代码:
\render -> mconcat ["<a href='", render Time, "'>The time</a>"]
复制代码
语法
所有莎士比亚语言都使用相同的插值语法,并且能够使用类型安全的URL。 它们的目标语言(HTML,CSS或Javascript)的语法不同。 让我们依次探索每种语言。
模板语法
Hamlet是最复杂的语言。 它不仅提供了生成HTML的语法,还允许基本的控制结构:条件,循环和maybes。
Html标签
显然,标签将成为任何HTML模板语言的重要组成部分。 在Hamlet中,我们尝试非常接近现有的HTML语法,以使语言更加舒适。 但是,我们使用缩进代替使用结束标记来表示嵌套。 HTML中就是这样的:
<body>
<p>Some paragraph.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</body>
复制代码
将变成:
<body>
<p>Some paragraph.
<ul>
<li>Item 1
<li>Item 2
复制代码
一般来说,一旦你习惯了它,我们发现这比HTML更容易理解。 唯一棘手的部分是在标签之前和之后处理空白。 例如,假设您要创建HTML
<p>Paragraph <i>italic</i> end.</p>
复制代码
我们希望确保在单词“Paragraph”之后和单词“end”之前保留空格。 为此,我们使用两个简单的转义字符:
<p>
Paragraph #
<i>italic
\ end.
复制代码
空白转义规则实际上非常简单: 如果一行中的第一个非空格字符是反斜杠,则忽略反斜杠。 (注意:这也会导致此行上的任何标记都被视为纯文本。) 如果一行中的最后一个字符是哈希值,则忽略它。 还有一件事。 哈姆雷特不会在其内容中逃避实体。 这样做是为了让现有的HTML更容易被复制。所以上面的例子也可以写成:
<p>Paragraph <i>italic</i> end.
复制代码
请注意,第一个标签将由哈姆雷特自动关闭,而内部“i”标签则不会。 您可以自由地使用您想要的任何方法,任何一种选择都不会受到惩罚。 但请注意,在Hamlet中使用结束标记的唯一时间是用于此类内联标记; 普通标签未关闭。 另一个结果是第一个标签之后的任何标签都没有对ID和类进行特殊处理。 例如,Hamlet片段:
<p #firstid>Paragraph <i #secondid>italic end.
#generates the HTML:
<p id="firstid">Paragraph <i #secondid>italic</i> end.</p>
复制代码
注意p标签是如何自动关闭的,其属性得到特殊处理,而i标签则被视为纯文本。
插值
到目前为止我们所拥有的是一个很好的简化HTML,但它根本不让我们与我们的Haskell代码进行交互。 我们如何传递变量? 简单:带插值:
<head>
<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
复制代码
我们备受吹捧的类型安全网址怎么样? 它们几乎与各种方式的变量插值相同,只是它们以at符号(@)开头。 此外,通过插入符号(^)进行嵌入,允许您嵌入另一个相同类型的模板。 下一个代码示例演示了这两个:
{-# 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
#This generates the expected HTML:
<p>You are currently on page 2.
<a href="/home?page=1">Previous</a>
<a href="/home?page=3">Next</a>
</p>
复制代码