FreeMarker
list
<#list animals as being>
<tr><td>${being.name}<td>${being.price} Euros
</#list>
include
<html>
<head>
<title>Test page</title>
</head>
<body>
<h1>Test page</h1>
<p>Blah blah...
<#include "/copyright_footer.html">
</body>
</html>
后面跟着一个!和默认值
<h1>Welcome ${user!"Anonymous"}!</h1>
在变量名后面通过放置??来询问FreeMarker一个变量是否存在
<#if user??><h1>Welcome ${user}!</h1></#if>
关于多级访问的变量,比如animals.python.price,书写代码:animals.python.price!0,仅当animals.python存在而仅仅最后一个子变量price可能不存在(这种情况下我们假设价格是0)。如果animals或者python不存在,那么模板处理过程将会以“未定义的变量”错误而停止。为了防止这种情况的发生,可以这样来书写代码(animals.python.price)!0。这种情况下当animals或python不存在时表达式的结果仍然是0。对于??也是同样用来的处理这种逻辑的:animals.python.price??对比(animals.python.price)??来看。
数值同时也可以含有多种类型,尽管这样很少使用。看下面这个数据模型mouse,就又是字符串又是哈希表。
如果用上面的数据模型合并到模板中,就该这么来写:
它的输出内容为:
<#if cargo.weight < 100>Light cargo</#if>
${cargo.weight / 2 + 100}
(root)
|
+- mouse = "Yerri"
|
+- age = 12
|
+- color = "brown"
${mouse} <#-- 用 mouse 作为字符串 -->
${mouse.age} <#-- 用 mouse 作为哈希表 -->
${mouse.color} <#-- 用 mouse 作为哈希表 -->
Yerri
12
brown
典型的应用是使用布尔值作为if指令的条件,比如<#if loggedIn>…</#if>
日期:日期变量可以存储和日期/时间相关的数据。一共有三种变化。
精确到天的日期(通常指的是“日期”),比如April 4, 2003
每天的时间(不包括日期部分),比如10:19:18 PM。时间的存储精确到毫秒。
日期-时间(也称作“时间戳”),比如April 4, 2003 10:19:18 PM。时间部分的存储精确到毫秒。
要注意一个数值也可有多种类型,对于一个数值可能存在哈希表和序列这两种类型,这时,该变量就支持索引和名称两种访问方式。不过容器基本是当作哈希表或者序列来使用的,而不是两者同时使用。
函数/方法和用户自定义指令的比较
这部分内容也是对高级用户来说的(如果你还不能理解可以先忽略它)。如果要使用函数/方法或自定义指令去实现一些东西的时候,二者之间的选择是两难的。按经验来说,
如果能够实现,请先用自定义指令而不要用函数/方法。指令的特征如下:
输出(返回值)的是标记(HTML,XML等)。主要原因是函数的返回结果可以自动进行XML转义(这是因为${…}的特性),而用户自定义指令的输出则不是(这是因为<@...>的特性所致,它的输出假定为是标记,因此就不再转义)。
副作用也是很重要的一点,它没有返回值。例如一个指令的目的是往服务器日志中添加一个条目。(事实上你不能得到自定义指令的返回值,但有些反馈的类型是有可能设置非本地变量的)
会进行流程的控制(就像list或if指令那样),但是不能在函数/方法上这么做。
应该意识到非常重要的一点:插值仅仅可以在文本中间使用(也可以在字符串表达式中,后续将会介绍)。
快速浏览(备忘单)
这里是给已经了解FreeMarker的人或有经验的程序员的一个提醒:
直接指定值
字符串:"Foo" 或者 'Foo' 或者 "It's \"quoted\"" 或者 r"C:\raw\string"
数字:123.45
布尔值:true, false
序列:["foo", "bar", 123.45], 1..100
哈希表:{"name":"green mouse", "price":150}
检索变量
顶层变量:user
从哈希表中检索数据:user.name, user[“name”]
从序列中检索:products[5]
特殊变量:.main
字符串操作
插值(或连接):"Hello ${user}!"(或"Free" + "Marker")
获取一个字符:name[0]
序列操作
连接:users + ["guest"]
序列切分:products[10..19] 或 products[5..]
哈希表操作
连接:passwords + {"joe":"secret42"}
算数运算: (x * 1.5 + 10) / 2 - y % 100
比较运算:x == y, x != y, x < y, x > y, x >= y, x <= y, x < y, 等等
逻辑操作:!registered && (firstVisit || fromEurope)
内建函数:name?upper_case
方法调用:repeat("What", 3)
处理不存在的值
默认值:name!"unknown" 或者(user.name)!"unknown" 或者name! 或者 (user.name)!
检测不存在的值:name?? 或者(user.name)??
参考:运算符的优先级
FreeMarker支持的所有转义字符。在字符串使用反斜杠的其他所有情况都是错误的,运行这样的模板都会失败。
转义序列
含义
\
引号(u0022)
\’
单引号(又称为撇号)(u0027)
\\
反斜杠(u005C)
\n
换行符(u000A)
\r
回车(u000D)
\t
水平制表符(又称为标签)(u0009)
\b
退格(u0008)
\f
换页(u000C)
\l
小于号:<
\g
大于号:>
\a
和号:&
\xCode
字符的16进制Unicode码(UCS码)
原生字符串。
一种特殊的字符串就是原生字符串。在原生字符串中,反斜杠和${没有特殊的含义,它们被视为普通的字符。为了表明字符串是原生字符串,在开始的引号或单引号之前放置字母r,例如:
将会打印:
It's "quoted" and
this is a backslash: \
It's "quoted" and
this is a backslash: \
${r"${foo}"}
${r"C:\foo\bar"}
${foo}
C:\foo\bar
序列
指定一个文字的序列,使用逗号来分隔其中的每个子变量,然后把整个列表放到方括号中。例如:
将会打印出:
列表中的项目是表达式,那么也可以这样做:[2 + 2, [1, 2, 3, 4], "whatnot"],其中第一个子变量是数字4,第二个子变量是一个序列,第三个子变量是字符串”whatnot”。
也可以用start..end定义存储数字范围的序列,这里的start和end是处理数字值表达式,比如2..5和[2, 3, 4, 5]是相同的,但是使用前者会更有效率(内存占用少而且速度快)。可以看出前者也没有使用方括号,这样也可以用来定义递减的数字范围,比如5..2。(此外,还可以省略end,只需5..即可,但这样序列默认包含5,6,7,8等递增量直到无穷大)
3.3.3.5 哈希表
在模板中指定一个哈希表,就可以遍历用逗号分隔开的“键/值”对,把列表放到花括号内。键和值成对出现并以冒号分隔。看这个例子:{"name":"green mouse", "price":150}。注意到名字和值都是表达式,但是用来检索的名字就必须是字符串类
<#list ["winter", "spring", "summer", "autumn"] as x>
${x}
</#list>
winter
spring
summer
autumn
也可以用start..end定义存储数字范围的序列,这里的start和end是处理数字值表达式,比如2..5和[2, 3, 4, 5]是相同的,但是使用前者会更有效率(内存占用少而且速度快)。可以看出前者也没有使用方括号,这样也可以用来定义递减的数字范围,比如5..2。(此外,还可以省略end,只需5..即可,但这样序列默认包含5,6,7,8等递增量直到无穷大)
变量名可以包含字母(也可以是非拉丁文),数字(也可以是非拉丁数字),下划线(_),美元符号($),at符号(@)和哈希表(#),此外要注意变量名命名时是不能以数字开头的。
(root)
|
+- book
| |
| +- title = "Breeding green mouses"
| |
| +- author
| |
| +- name = "Julia Smith"
| |
| +- info = "Biologist, 1923-1985, Canada"
|
+- test = "title"
如果我们想指定同一个表达式的子变量,那么还有另外一种语法格式:book["title"]。在方括号中可以给出任意长度字符串的表达式。在上面这个数据模型示例中你还可以这么来获取title:book[test] ,下面这些示例它们含义都是相等的:book.author.name, book["author"].name, book.author.["name"], book["author"]["name"]。
当使用点式语法时,顶层变量名的命名也有相同的限制(命名时只能使用字母,数字,下划线,$,@等),而使用方括号语法形式时命名有没有这样的限制,它可以是任意的表达式。(为了FreeMarker支持XML,如果变量名是*(星号)或者**,那么就应该使用方括号语法格式。)
${"Hello ${user}!"}
${"${user}${user}${user}${user}"}
Hello Big Joe!
Big JoeBig JoeBig JoeBig Joe
${"Hello " + user + "!"}
${user + user + user + user}
警告:
使用者在使用插值时经常犯的一个错误是:在不能使用插值的地方使用了它。插值只能
${"Hello ${user}!"}
${"${user}${user}${user}${user}"}
Hello Big Joe!
Big JoeBig JoeBig JoeBig Joe
${"Hello " + user + "!"}
${user + user + user + user}
在文本区段(<h1>Hello ${name}!</h1>)和字符串文字(<#include "/footer/${company}.html">)中使用。
序列的连接可以使用+号来进行,例如:
将会打印出:
${user[0]}
${user[4]}
B
J
<#list ["Joe", "Fred"] + ["Julia", "Kate"] as user>
- ${user}
</#list>
- Joe
- Fred
- Julia
- Kate
序列切分
使用[firstindex..lastindex]可以获取序列中的一部分,这里的firstindex和lastindex表达式的结果是数字。如果seq存储序列"a", "b", "c", "d", "e", "f",那么表达式seq[1..4]将会是含有"b", "c", "d", "e"的序列(索引为1的项是"b",索引为4的项是"e")。
lastindex可以被省略,那么这样将会读取到序列的末尾。如果seq存储序列"a", "b", "c", "d", "e", "f",那么seq[3..]将是含有"d", "e", "f"的序列。
像连接字符串那样,也可以使用+号的方式来连接哈希表。如果两个哈希表含有键相同的项,那么在+号右侧的哈希表中的项目优先。例如:
将会打印出:
注意很多项目连接时不要使用哈希表连接,比如在循环时往哈希表中添加新项。这和序列连接的情况是一致的。
3.3.8 算数运算
算数运算包含基本的四则运算和求模运算,运算符有:
加法:+
<#assign ages = {"Joe":23, "Fred":25} + {"Joe":30, "Julia":18}>
- Joe is ${ages.Joe}
- Fred is ${ages.Fred}
- Julia is ${ages.Julia}
- Joe is 30
- Fred is 25
- Julia is 18
但这种情况也有一个例外,就是+号,它是用来连接字符串的,如果+号的一端是字符串,另外一端是数字,那么数字就会自动转换为字符串类型(使用适当的格式)。示例如下:
将会输出:
通常来说,FreeMarker不会自动将字符串转换为数字,反之会自动进行。
有时我们只想获取计算结果的整数部分,这可以使用内建函数int来解决。(关于内建函数后续章节会来解释)
仍然假设x的值是5,那么将会输出:
75
2.5
2
${3 * "5"} <#-- WRONG! -->
${3 + "5"}
35
有时我们只想获取计算结果的整数部分,这可以使用内建函数int来解决。(关于内建函数后续章节会来解释)
仍然假设x的值是5,那么将会输出:
75
2.5
2
${3 * "5"} <#-- WRONG! -->
${3 + "5"}
35
${(x/2)?int}
${1.1?int}
${1.999?int}
${-1.1?int}
${-1.999?int}
对数字和日期类型的比较,也可以使用<,<=,>=和>。不能把它们当作字符串来比较
使用>=和>的时候有一点小问题。FreeMarker解释>的时候可以把它当作FTL标签的结束符。为了避免这种问题,不得不将表达式放到括号内:<#if (x > y)>,或者可以在比较关系处使用>和<:<#if x > y>
在参考文档中可以查到所有内建函数的资料。现在,我们只需了解一些重要的内建函数就行了。
字符串使用的内建函数:
html: 字符串中所有的特殊HTML字符都需要用实体引用来代替(比如<代替<)。
cap_first:字符串的第一个字母变为大写形式
lower_case:字符串的小写形式
upper_case:字符串的大写形式
trim:去掉字符串首尾的空格
序列使用的内建函数:
size:序列中元素的个数
数字使用的内建函数:
int:数字的整数部分(比如-1.9?int就是-1)
示例:
假设字符串test存储”Tom & Jerry”,那么输出为:
注意test?upper_case?html,内嵌函数双重使用,test?upper_case的结果是字符串了,但也还可以继续在其后使用html内建函数。
另外一个例子:
假设seasons存储了序列"winter", "spring", "summer", "autumn",那么上面的输出将会是:
<#if x < 12 && color = "green">
We have less than 12 things, and they are green.
</#if>
<#if !hot> <#-- here hot must be a boolean -->
It's not hot.
</#if>
${test?html}
${test?upper_case?html}
假设字符串test存储”Tom & Jerry”,那么输出为:
注意test?upper_case?html,内嵌函数双重使用,test?upper_case的结果是字符串了,但也还可以继续在其后使用html内建函数。
另外一个例子:
假设seasons存储了序列"winter", "spring", "summer", "autumn",那么上面的输出将会是:
4
Spring
Horse
处理不存在的值
要注意这个操作是FreeMarker 2.3.7版本以后才有的(用来代替内建函数default,exists和if_exists)。
正如我们前面解释的那样,当访问一个不存在的变量时FreeMarker将会报错而导致模板执行中断。通常我们可以使用两个操作符来压制这个错误,控制错误的发生。被控制的变量可以是顶层变量,哈希表或序列的子变量。此外这些操作符还能处理方法调用的返回值不存在的情况(这点对Java程序员来说,返回值是null而不是返回值为void类型的方法),通常来说,我们应该使用这些操作符来控制可能不存在的变量。
对于知道Java中null的人来说,FreeMarker 2.3.x版本把它们视为不存在的变量。简单地说,模板语言中没有null这个概念。比如有一个bean,bean中有一个maidenName属性,对于模板而言(假设你没有配置FreeMarker来使用一些极端的对象包装),这个属性的值是null,和不存在这个属性的情况是一致的。调用方法的返回值如果是null的话FreeMarker也会把它当作不存在的变量来处理(假定你只使用了普通的对象包装)。了解更多可以参考FAQ中的相关内容。
默认值可以是任何类型的表达式,也可以不必是字符串。你也可以这么写:hits!0或colors!["red", "green", "blue"]。默认值表达式的复杂程度没有严格限制,你还可以这么来写:cargo.weight!(item.weight * itemCount + 10) 。
如果默认值被省略了,那么结果将会是空串,空序列或空哈希表。(这是FreeMarker允许多类型值的体现)如果想让默认值为0或false,则注意不能省略它。例如:
输出为:
()
(Jerry)
因为语法的含糊<@something a=x! b=y />将会解释为<@something a=x!(b=y) />,那就是说b=y将会视为是比较运算,然后结果作为x的默认值。而不是想要的参数b是x的默认值,为了避免这种情况,如下编写代码:<@something a=(x!) b=y />。
在不是顶层变量时,默认值操作符可以有两种使用方式:
如果是这样的写法,那么在product中,当color不存在时(返回”red”)将会被处理,但是如果连produce都不存在时将不会处理。也就是说这样写时变量product必须存在,否则模板就会报错。
这时,如果当不存在时也会被处理,那就是说如果product不存在或者product存在而color不存在,都能显示默认值”red”而不会报错。本例和上例写法的重要区别在于用括号时,就允许其中表达式的任意部分可以未定义。
检测不存在的值
使用形式概览:unsafe_expr??或(unsafe_expr)??
这个操作符告诉我们一个值是否存在。基于这种情况,结果是true或false。
示例如下,假设并没有名为mouse的变量:
输出为:
访问非顶层变量的使用规则和默认值操作符也是一样的,即product.color??和(product.color)?? 。
<#assign seq = ['a', 'b']>
${seq[0]!'-'}
${seq[1]!'-'}
${seq[2]!'-'}
${seq[3]!'-'}
a
b
-
-
<#if mouse??>
Mouse found
<#else>
No mouse found
</#if>
Creating mouse...
<#assign mouse = "Jerry">
<#if mouse??>
Mouse found
<#else>
No mouse found
</#if>
No mouse found
Creating mouse...
Mouse found
插值的使用语法是:${expression},expression可以是所有种类的表达式(比如${100 + x})。
插值是用来给插入具体值然后转换为文本(字符串)。插值仅仅可以在两种位置使用:文本区(如<h1>Hello ${name}!</h1>)和字符串表达式(如<#include "/footer/${company}.html">)中。
字符串插入指南:不要忘了转义! 如果插值在文本区(也就是说,不再字符串表达式中),如果escapse指令起作用了,即将被插入的字符串会被自动转义。如果你要生成HTML,那么强烈建议你利用它来阻止跨站脚本攻击和非格式良好的HTML页面。这里有一个示例:
<#escape x as x?html>
...
<p>Title: ${book.title}</p>
<p>Description: <#noescape>${book.description}</#noescape></p>
<h2>Comments:</h2>
<#list comments as comment>
<div class="comment">
${comment}
</div>
</#list>
...
</#escape>
这个示例展示了当生成HTML时,你最好将完整的模板放入到escape指令中。那么,如果book.title包含&,它就会在输出中被替换成&,而页面还会保持格式良好的HTML。如果用户注释包含如<iframe>(或其它的元素)的标记,那么就会被转义成如<iframe>的样子,使他们没有任何有害点。但是有时在数据模型中真的需要HTML,比如我们假设上面的book.description在数据库中的存储是HTML格式的,那么此时你不得不使用noescape来抵消escape的转义,模板就会像这样了:
这和之前示例的效果是一样的,但是这里你可能会忘记一些?html内建函数,就会有安全上的问题了。在之前的示例中,你可能忘记一些noescape,也会造成不良的输出,但是起码是没有安全隐患的。
数字插入指南
如果表达式是数字类型,那么根据数字的默认格式,数值将会转换成字符串。这也许会包含最大小数,数字分组和相似处理的问题。通常程序员应该设置默认的数字格式,而模板设计者不需要处理它(但是可以使用number_format设置来进行,详情参考setting指令部分的文档)。你可以使用内建函数string为一个插值来重写默认数值格式。
小数的分隔符通常(其他类似的符号也是这样,如分组符号)是根据所在地的标准(语言,国家)来确定的,这也需要程序员来设置。例如这个模板:
如果当前地区设置为英国时,将会打印:
而如果当前的地区设置为匈牙利时,将会打印:
而当前地区为匈牙利时,将会打印:
这是因为匈牙利人使用逗号作为小数点。
可以使用内建函数string为单独的插值来修改设置。
警告:
可以看出,插值的打印都是给用户看的,而不是给计算机的。有时候这样并不好,比如你要打印数据库记录的主键,用来作为URL中的一部分或HTML表单的隐藏域来作为提交内容,或者要打印CSS/JavaScript的数字。这些值都是给计算机程序去识别的而不是用户,很多程序对数字格式的要求非常严格,它们只能理解一部分简单的美式数字格式。那样的话,可以使用内建函数c(代表计算机)来解决这个问题,比如:
${1.5}
1.5
1,5
<a href="/shop/productdetails?id=${product.id?c}">Details...
</a>
...
<p>Title: ${book.title?html}</p>
<p>Description: ${book.description}</p>
<h2>Comments:</h2>
<#list comments as comment>
<div class="comment">
${comment?html}
</div>
</#list>
...
数字插入指南
如果表达式是数字类型,那么根据数字的默认格式,数值将会转换成字符串。这也许会包含最大小数,数字分组和相似处理的问题。通常程序员应该设置默认的数字格式,而模板设计者不需要处理它(但是可以使用number_format设置来进行,详情参考setting指令部分的文档)。你可以使用内建函数string为一个插值来重写默认数值格式。
小数的分隔符通常(其他类似的符号也是这样,如分组符号)是根据所在地的标准(语言,国家)来确定的,这也需要程序员来设置。例如这个模板:
如果当前地区设置为英国时,将会打印:
而如果当前的地区设置为匈牙利时,将会打印:
而当前地区为匈牙利时,将会打印:
这是因为匈牙利人使用逗号作为小数点。
可以使用内建函数string为单独的插值来修改设置。
警告:
可以看出,插值的打印都是给用户看的,而不是给计算机的。有时候这样并不好,比如你要打印数据库记录的主键,用来作为URL中的一部分或HTML表单的隐藏域来作为提交内容,或者要打印CSS/JavaScript的数字。这些值都是给计算机程序去识别的而不是用户,很多程序对数字格式的要求非常严格,它们只能理解一部分简单的美式数字格式。那样的话,可以使用内建函数c(代表计算机)来解决这个问题,比如:
${1.5}
1.5
1,5
警告:
可以看出,插值的打印都是给用户看的,而不是给计算机的。有时候这样并不好,比如你要打印数据库记录的主键,用来作为URL中的一部分或HTML表单的隐藏域来作为提交内容,或者要打印CSS/JavaScript的数字。这些值都是给计算机程序去识别的而不是用户,很多程序对数字格式的要求非常严格,它们只能理解一部分简单的美式数字格式。那样的话,可以使用内建函数c(代表计算机)来解决这个问题,比如:
${1.5}
1.5
1,5
<a href="/shop/productdetails?id=${product.id?c}">Details...
</a>
...
<p>Title: ${book.title?html}</p>
<p>Description: ${book.description}</p>
<h2>Comments:</h2>
<#list comments as comment>
<div class="comment">
${comment?html}
</div>
</#list>
...
布尔值插入指南
若要使用插值方式来打印布尔值会引起错误,中止模板的执行。例如:${a == 2}就会引起错误,它不会打印”true”或其他内容。
然而,我们可以使用内建函数string来将布尔值转换为字符串形式。比如打印变量”married”(假设它是布尔值),那么可以这么来写: ${married?string("yes", "no")}。