shabbyrobe / tempe
非常简单、灵活的 PHP 模板语言
Requires (Dev)
This package is not auto-updated.
Last update: 2022-10-24 10:32:05 UTC
README
Tempe (temˈpē) 是一个非常简单的模板语言。
这个名字是为了纪念悉尼的一个郊区,我在开车时决定了它的运作方式。灵感故事,对吧!
它提供了非常简单的原语,可以组合成您自己的特定领域的、简单但功能丰富的模板语言,并且还附带了一个预构建的通用语言,它提供的语义对 handlebars.js 用户来说可能有些熟悉。
如果您想要一个简单、灵活的模板引擎,与 Mustache/Handlebars 或 Twig/Jinja2 相当,请参阅“语言”部分。
如果您想使用 Tempe 的原语创建自己的简单、特定领域的模板语言,请参阅 guts_。
快速入门
使用 composer 安装
composer require shabbyrobe/tempe:2.*@beta
典型的 Tempe 模板
Hello {= get name }
You have just won {= get value } dollars!
{# get in_ca | show }
Well, {= get taxed_value } dollars, after taxes.
{/ }
给定以下哈希值
{
"name": "Chris",
"value": 10000,
"taxed_value": 10000 - (10000 * 0.4),
"in_ca": true
}
将生成以下内容
Hello Chris
You have just won 10000 dollars!
Well, 6000.0 dollars, after taxes.
PHP 代码
<?php
$lang = Tempe\Lang\Factory::createBasic();
$renderer = new Tempe\Renderer($lang);
$scope = [
'foo' => 'hello',
'bar' => 'world',
];
$renderer->render('{= get foo } {= get bar }!', $scope);
为什么?
通常的原因:对现有选项不满意。
Mustache <http://mustache.github.io/>
_ 是一个好主意,但它过于限制。总有“逻辑”的小片段,其实并不是真正的逻辑,但你仍然需要。情境特定的转义过滤器会立刻想到。
Twig <http://twig.sensiolabs.com/>
_ 功能非常强大,但它有自己的问题。复杂的模板由于允许复杂的表达式逻辑轻易地从控制器流入模板,因此可能非常难以处理。这是不守纪律的开发者的易受攻击之处(我在看着你们每一个人,尤其是你,时钟)。
这两个模板引擎都是不错的选择,但我已经花了太多时间在与两种方法的问题作斗争。Tempe 是寻找中间地带的实验。移除 Twig 中最糟糕的杂乱逻辑,保持原语尽可能简单,就像 Mustache 一样。
Tempe 的 Guts 也比 Mustache 和 Twig 简单得多 - 它默认不提供任何功能,但它的原语使您能够提供功能完整的替代方案。Tempe 随附了这些功能的实现,如果您愿意可以使用,但您不必这么做 - 您可以自由地按照自己的方式实现自己的处理器。
由于这种设计,Tempe 还可以用来创建自己的模板 DSL - 您可以将其简化到几乎什么都没有,并根据需要进行定制。这使得它在您不希望提供复杂的模板系统但需要比 strtr
提供的更多一点时(这正是我最初编写它的原因 - strtr
太简单,Twig 不够简单,Mustache 太 HTML 特定)非常理想。
原语
Tempe 有三个原语 - 值标签、块标签和转义序列。
值和块标签
值标签旨在完全替换,看起来像这样
{= chain }
块标签用于包围和捕获模板部分
{# chain }contents{/ }
块标签可以嵌套到任意深度
{# chain }{# chain }{/ }{/ }
块可以命名,以便更容易识别关闭标签
{# b1: chain } {# b2: chain } {/ b2 } {/ b1 }
标签定界符不能更改。
处理链
块和值标签都可以包含一个处理链。
处理链类似于Unix管道 - 一个处理器的输出被发送到下一个处理器的输入。链中的最后一个处理器连接到渲染器的输出。
每个处理器之间由管道分隔。您可以按需连接任意数量的处理器。
{= handler | handler | handler }
处理器由一个或多个标识符组成。标识符必须满足以下正则表达式
[a-zA-Z_\/\.\-\d]+
第一个标识符被认为是处理器名称,所有后续标识符被认为是参数
{= handler1 arg1 arg2 | handler2 arg1 arg2 }
标识符和管道之间的空白将被忽略。以下标签是相同的
{=handler1 arg1 arg2|handler2|handler3}
{= handler1 arg1 arg2 | handler2 | handler3 }
{= handler1 arg1 arg2 |
handler2 | handler3 }
"空白"等同于PCRE \s
转义序列(LF,CR,FF,HTAB,空格)。
只包含空白的标签和空标签是允许的。这可以用于基本的空白控制
{=}
{=
}
{# }{/ }
您可以使用空块来模拟模板注释。这不会影响解析器,只会影响渲染器
{#}This will not appear{/}
转义序列
当您想在输出中包含标签开头的字面值时,需要使用转义序列。
只需在标签开头加上分号即可将其转换为转义序列。
{=;
在输出中变为{=
{#;
在输出中变为{#
{/;
在输出中变为{/
例如,以下模板
Value tags look like this: {=; foo }
Block tags look like this: {#; id: foo }bar{/; foo }
将产生以下输出
Value tags look like this: {= foo bar }
Block tags look like this: {# id: foo }bar{/ foo }
语言
Play
Tempe附带了一个为boris <https://github.com/d11wtq/boris>
配置的文件。Boris提供了一个PHP REPL。如果您从Tempe源目录调用boris
,您将获得一个已设置并准备就绪的Tempe shell
~/php/tempe$ boris
Tempe Shell
[1] boris> dumptpl("{= get foo }");
0 1 P_ROOT |
1 1 P_VALUE | get (foo)
→ NULL
[2] boris> render("{= get foo }", ['foo'=>'bar']);
Render:
---
bar
---
Parser time: 0.306ms
Render time: 0.481ms
→ NULL
处理器
获取变量foo
并将其写入输出
{= get foo }
获取变量foo
,将其作为HTML转义,然后写入输出
{= get foo | as html }
嵌套转义上下文可以在一个as
调用中处理
<a href="url.php?arg={= get foo | as html urlquery }">foo</a>
警告:Tempe默认不执行任何转义。模板作者必须始终了解他们发出值的上下文。
Pádraic Brady的文章Automatic Output Escaping in PHP and the Real Future of Preventing Cross-Site Scripting (XSS) <https://web.archive.org/web/20160108153856/http://blog.astrumfutura.com/2012/06/automatic-output-escaping-in-php-and-the-real-future-of-preventing-cross-site-scripting-xss/>
是那些认为自动输出转义不是一个坏主意的人必读的。
嵌套变量查找
Given the hash {"foo": {"bar": "yep"}
This should print "yep": {= get foo | get bar }
将块的内容设置为一个变量
Should print nothing: {# set foo }Hello World{/}
Should print "Hello World": {= get foo }
从一个不同的变量设置一个变量,如果它已经存在,则覆盖它
{# set foo }hello{/}
{# set bar }world{/}
{= get foo | set bar }
Should print hello: {= get bar }
如果变量foo
是真实的,则显示一个块
{# get foo | show }Truthy!{/}
如果变量foo
等于值hello
,则显示一个块
{# get foo | eq hello | show }Hello!{/}
如果变量foo
不等于值hello
,则显示一个块
{# get foo | eq hello | not | show }Goodbye!{/}
eq
仅限于对标识符进行松散比较。可以使用eqvar
在变量之间进行比较
Given the hash {"foo": "yep", "bar": "yep"}
This block should render:
{# get foo | eqvar bar | show }foo is equal to bar!{/}
可以使用set
和eqvar
的组合来测试复杂表达式。这允许在比较中使用连接
{# set foo }hel{/}
{# set bar }lo{/}
{# set expr}{= get foo }{= get bar }{/}
{# set test }hello{/}
{# get expr | eqvar test | show }This should show!{/}
块迭代
With the following hash:
{"foo": [ {"a": 1, "b": 2}, {"a": 3, "b": 4} ]}
This template:
{# each foo }
Key: {= get _key_ }
Value: {= get _value_ | get a }
0-based index: {= get _idx_ }
1-based number: {= get _num_ }
Is it first?: {# get _first_ | show }Yep!{/}{# get _first_ | not |show }Nup!{/}
`foo` is merged with the current scope:
{= get a }, {= get b }
{/}
Will output:
Key: 0
Value: 1
0-based index: 0
1-based number: 1
Is it first?: Yep!
`foo` is merged with the current scope:
1, 2
Key: 1
Value: 3
0-based index: 1
1-based number: 2
Is it first?: Nup!
`foo` is merged with the current scope:
3, 4
将数组推送到当前作用域的块
Given the hash: {"foo": {"bar": "hello"}
The template: {# push foo }{= get bar }{/}
Should output: hello
使用push
构建嵌套数组
{# a: push foo }
{# b: push bar }
{# set baz }hello{/}
{/ b }
{/ a }
Should print 'hello': {= get foo | get bar | get baz }
处理器是可连接的。以下构造示例将整个块转换为大写,然后对其进行HTML转义,然后将其设置到另一个变量中
{# show | upper | as html | set foo }
foo & bar
{/}
Should show "FOO & BAR": {= get foo }
处理器参考
注意:在定义处理器语法时使用以下约定
方括号
[...]
内的是可选的。如果处理器名称前面有一个参数和一个管道,则处理器操作管道的输入。例如,
<key> | eat
表示eat
处理器从输入中获取一个<key>
。
Tempe作为其核心语言的一部分提供了以下处理器
get
获取当前作用域中键的值。
语法:[ <key> | ] get [ <key> ]
输出:mixed
有效上下文:value,block
需要键。键可以作为参数传递,也可以通过输入传递。通过参数传递的键具有优先级。
查找可以嵌套。以下输出 hello
render("{= get foo | get bar }", ['foo'=>['bar'=>'hello']]);
set
将当前作用域中键的值设置为输入值。
语法:[ <input> | ] set <key>
输出:null
有效上下文:value,block
需要 <key>
。
输入总是来自管道。如果 set
处理器在链中首先出现,则输入将为空字符串。
eq
将输入与标识符进行比较,并输出 true 或 false。
语法:<input> | eq <compare>
输出:布尔值
有效上下文:value,block
这仅允许简单的相等比较 - 可以用作标识符的任何内容都可以用于 <compare>
。对于更复杂的相等比较,请使用 eqvar
。
此处理器主要用于影响其他处理器,如 show
:
{# get foo | eq hello | show } Will show if 'foo'=='hello'! {/}
eqvar
将输入与当前作用域中键的值进行比较,并输出 true 或 false。
语法:<input> | eqvar <key>
输出:布尔值
有效上下文:value,block
这通过从当前作用域中获取 <key>
的值允许更复杂的相等比较。
如果比较值不在作用域中,则创建它:
{# set test }HELLO!.{/}
{# get foo | eqvar test | show }
Will show if 'foo' == 'HELLO!'
{/}
not
否定输入的真值。
语法:<input> | not
输出:布尔值
有效上下文:value,block
示例:
{# get foo | not | show }
If foo is not truthy, this will show
{/}
each
为输入或作用域键中的每个项目渲染一个块。
语法
<input> | each
each <key>
输出:字符串(渲染的模板)
有效上下文:块
以下变量在每个迭代中可用
_key_
:当前键_value_
:当前值_idx_
:基于0的索引_num_
:基于1的数字_first_
:指示第一个项目的布尔值
as
使用提供的上下文转义输入
语法:<input> | as <context>
可用的转义上下文
- cssString
- html
- htmlAttr
- htmlComment
- htmlAttrUnquoted
- js
- jsQuoted
- urlQuery
- xml
- xmlAttr
- xmlComment
有效上下文:块
show
渲染一个块
语法
<input> | show
show
push
将键的值推送到当前作用域,并渲染该块。
语法:push <key>
输出:字符串(渲染的内容)
有效上下文:块
字符串过滤器
upper
:转换为大写lower
:转换为小写ucfirst
:第一个字符串转换为大写lcfirst
:第一个字符串转换为小写ucwords
:每个单词的第一个字母转换为大写trim
:从字符串两端删除所有空白ltrim
:从开头删除空白rtrim
:从末尾删除空白rev
:反转字符串striptags
:从字符串中删除HTML标签(PHP函数)base64
:转换为base64nl2spc
:将一个或多个连续的换行符转换为单个空格nl2br
:将每个换行符转换为<br />
缓存
Tempe本身不进行缓存,但您可以自己缓存解析树
<?php
$lang = Tempe\Lang\Factory::createBasic();
$parser = new Tempe\Parser($lang);
$renderer = new Tempe\Renderer($lang, $parser);
$tpl = '{= get foo } {= get bar }!';
$tree = get_the_tree_from_cache($tpl);
if (!$tree) {
$tree = $parser->parse($tpl);
cache_the_tree_pls($tpl, $tree);
}
$scope = [
'foo' => 'hello',
'bar' => 'world',
];
$renderer->renderTree($tree, $scope);
内部结构
使用Tempe的原始方法制作自己的语言非常简单,您只需编写自己的处理器
<?php
$handlers = [
'foo'=>function($handler, $in, \Tempe\HandlerContext $context) { return 'foo'; },
'bar'=>function($handler, $in, \Tempe\HandlerContext $context) { return 'bar'; },
];
$lang = new \Tempe\Lang\Basic($handlers);
$renderer = new \Tempe\Renderer($lang);
echo $renderer->render('{= foo }{= bar }');
注意:上述处理器包含一种相当冗长的方式来表示参数。本指南的其余部分将简单地使用 ($h, $in, $ctx)
作为 ($handler, $in, \Tempe\HandlerContext $context)
的缩写。
处理器函数
处理器函数接受三个参数
$handler
包含以下属性的对象
name
:处理器名称args
:处理器的参数数组argc
:参数数量
给定模板 {= h 1 2 3 }
,name
将被设置为 h
,args
将被设置为 [1, 2, 3]
,而 argc
将被设置为 3。
$in
包含链中任何先前处理器提供的输入(如果处理器是第一个,则为空字符串)。这与 Unix 中 STDIN
的工作方式非常相似。处理器可以返回任何内容,所以如果你想进行合理的错误处理(不仅仅是“无法将 BlahBlah 类的对象转换为字符串”之类的垃圾),请确保包含一些合理性检查。
$context
一个 Tempe\HandlerContext
实例,它具有以下属性
renderer
The renderer which is calling the handler will be available here. You may call
`render` against it without any ill effects.
scope
array or ArrayAccess instance containing the current scope.
chainPos
0-indexed position of this handler in the chain.
break
Boolean, default `false`. Set this to `true` if you want each subsequent
handler in the chain to be ignored. You may still return a value from the handler
even if you set break to `true`.
node
The node in the parse tree corresponding to this handler's tag. Use this, combined
with `renderer`, to recurse::
$myHandler = function($handler, $in, $context) {
return $context->renderer->renderTree($context->node, $context->scope);
};
You may replace, modify or omit `$context->scope` if you wish.
Nodes
传递给处理器的 HandlerContext
包含与处理器标签对应的解析树中的节点。节点对象包含以下属性
type
:要么是\Tempe\Render::P_BLOCK
,要么是\Tempe\Renderer::P_VALUE
。line
:此标签在模板中打开的行。id
:如果标签包含 id(冒号{= myid: handler }
之前的部分),则此处将可用,否则为null
。chain
:处理器的整个链,作为一个处理器对象的数组。处理器对象在处理器函数中描述。
如果节点的类型是 \Tempe\Render::P_BLOCK
,它还将具有 nodes
属性。它将包含一个表示块内容的节点数组。
递归
Tempe\Renderer
不会自动递归块标签
<?php
$handlers = [
'foo'=>function($h, $in, $ctx) { return 'foo'; },
'bar'=>function($h, $in, $ctx) { throw new \Exception(); },
];
$lang = new \Tempe\Lang\Basic($handlers);
$renderer = new \Tempe\Renderer($lang);
echo $renderer->render('{# foo }{= bar }{/}');
上面的示例打印 foo
。异常永远不会被触发。如果你想编写一个返回块内容的处理器,可以使用 HandlerContext
来递归渲染节点
<?php
$handlers = [
'foo'=>function($h, $in, $ctx) {
return $ctx->renderer->renderTree($ctx->node, $ctx->scope);
},
'bar'=>function($h, $in, $ctx) { return 'bar'; },
];
$lang = new \Tempe\Lang\Basic($handlers);
$renderer = new \Tempe\Renderer($lang);
echo $renderer->render('{# foo }{= bar }{/}');
这次我们得到 bar
作为输出。
如果你没有将 $ctx->scope
作为 renderTree
的第二个参数传递,你将失去对块内当前作用域的访问。这可能正是你想要的,但这可能不是最好的选择。你可以在传递给 renderTree
之前随意修改作用域。
如果你计划在块中进行修改,你应该注意使用数组和使用 ArrayAccess 实例作为作用域之间的区别
<?php
$handlers = [
'block'=>function($h, $in, $ctx) {
$scope = $ctx->scope;
$scope['foo'] = 'inside';
return $ctx->renderer->renderTree($ctx->node, $scope);
},
'get'=>function($h, $in, $ctx) { return $ctx->scope[$h->args[0]]; },
];
$renderer = new \Tempe\Renderer(new \Tempe\Lang\Basic($handlers));
$tpl = "{# block }{= get foo }{/} {= get foo }";
$scope = ['foo'=>'outside'];
assert("inside outside" == $renderer->render($tpl, $scope));
$scope = new \ArrayObject(['foo'=>'outside']);
assert("inside inside" == $renderer->render($tpl, $scope));
规则
你可以在处理器中直接实现所有的验证作为守卫子句。如果子句失败,你应该抛出 \Tempe\Exception\Check
。如果你将节点的行作为第二个参数传递,你将获得更好的错误消息。
<?php
$lang = new \Tempe\Lang\Basic(['myHandler'=>function($h, $in, $ctx) {
if ($h->argc != 1) {
$msg = "myHandler expects 1 argument, found {$h->argc}";
throw new \Tempe\Exception\Check($msg, $ctx->node->line);
}
if ($ctx->chainPos != 0) {
$msg = "myHandler must be first in a chain, found at pos {$ctx->chainPos}";
throw new \Tempe\Exception\Check($msg, $ctx->node->line);
}
return $h->args[0];
}]);
如果你有很多处理器,这可能会变得很繁琐,此外,如果每个处理器调用都进行大量检查,它还会减慢渲染速度。
更好的检查位置是在解析期间。Tempe\Lang\Basic
提供了一种指定最常见规则简单的方法,但也可以传递任意的检查函数。这些规则将在解析时应用。
<?php
$handlers = [
'myHandler'=>function($h, $in, $ctx) {
return $h->args[0];
}
];
$rules = [
'myHandler'=>['argc'=>1, 'first'=>true],
];
$lang = new \Tempe\Lang\Basic($handlers, $rules);
// if you are creating the parser by hand, you must pass the language
$parser = new \Tempe\Parser($lang);
$renderer = new \Tempe\Renderer($lang, $parser);
// if you are allowing the renderer to create the default parser for you,
// the language will also be passed.
$renderer = new \Tempe\Renderer($lang);
// throws "Handler 'myHandler' expected 1 arg(s), found 2 at line 1"
$renderer->render('{= myHandler a b }');
// throws "Handler 'myHandler' expected to be first, but found at pos 2 at line 1
$renderer->render('{= myHandler a | myHandler a b }');
如果你喜欢,也可以指示渲染器在渲染时进行检查。这可能很有用,如果你想要缓存解析树并确保在渲染期间仍然有效,但这会减慢渲染速度,因此默认情况下是关闭的。
<?php
$renderer = new \Tempe\Renderer($lang, $parser, !!'check');
// use the default lang and parser
$renderer = new \Tempe\Renderer(null, null, !!'check');
// set it as a property instead
$renderer = new \Tempe\Renderer();
$renderer->check = true;
可用规则
argc
- int
处理器参数数量必须恰好等于此值
argMin
- int
处理器参数数量不得少于此值。如果设置了 argc
,则忽略。
argMax
- int
处理器参数数量不得多于此值。如果设置了 argc
,则忽略。
allowValue
- bool,默认:true
将此设置为 false 以防止在 value 标签上使用处理器
allowBlock
- 布尔值,默认:true
将此设置为false以防止处理程序在块标签上使用
chainable
- 布尔值,默认:true
如果您希望这是链中唯一的处理程序,则将其设置为false。如果处理程序lonesome
的chainable
为false:
Valid:
{= lonesome }
{# lonesome }{/}
Invalid:
{= foo | lonesome | bar }
{= lonesome | bar }
{= bar | lonesome }
last
- 布尔值,默认:null
如果为true
,则链中不会跟有其他处理程序。有效:{= foo | mustbelast }
。无效:{= foo | mustbelast | bar }
。
如果为false
,则此处理程序不应是链中的最后一个。有效:{= foo | mustnotbelast | bar }
。无效:{= foo | bar | mustnotbelast }
。
first
- 布尔值,默认:null
如果为true
,则此处理程序必须是链中的第一个处理程序。有效:{= mustbefirst | foo }
。无效:{= foo | mustbefirst }
如果为false
,则此处理程序不应是链中的第一个。有效:{= foo | mustnotbefirst }
。无效:{= mustnotbefirst }
。
check
- 可调用
将您喜欢的任何函数传递到此。它将接收以下参数:
function check($handler, $node, $chainPos)
您必须返回true
以便处理程序通过。如果您返回一个假的值或什么也不返回,您将收到一个通用的异常,这可能并不特别有帮助。
为了您的用户,您应该抛出带有描述性信息的Tempe\Exception\Check
异常。
<?php
$handlers = [
'foo'=>function($handler) {
return $handler->args[0];
},
];
$rules = [
'foo'=>['check'=>function($handler, $node, $chainPos) {
if ($handler->args[0] != 'foo') {
$msg = "For some reason, you can only pass 'foo' as the first argument";
throw new \Tempe\Exception\Check($msg, $node->line);
}
return true;
}],
];
$lang = new \Tempe\Lang\Basic($handlers, $rules);
解析
Tempe\Parser
将模板转换为解析树。
展示解析器如何工作的最好方式可能是向您展示Tempe\Helper::dumpNode($node)
的输出。
<?php
$tpl = "
Here's a value tag. The handler is 'hello':
{= hello world }
Here's a chained value tag:
{= foo bar | baz qux | ding dang dong }
Ooh, escape sequence:
{=; foo bar }
{#; foo bar }{/; }
Here's a named block tag with some stuff inside:
{# mystuff: group }
{= pants }
{# morestuff }{= pants }{/}
{/ mystuff }
";
$parser = new \Tempe\Parser();
\Tempe\Helper::dumpNode($parser->parse($tpl));
输出(列分别是深度、行、类型或id、信息):
0 1 P_ROOT |
1 1 P_STRING | "Here's a value tag. ..."
1 2 P_VALUE | hello (world)
1 2 P_STRING | "\n\nHere's a chained v..."
1 5 P_VALUE | foo (bar) -> baz (qux) -> ding (dang dong)
1 5 P_STRING | "\n\nOoh, escape sequen..."
1 8 P_ESC |
1 8 P_STRING | "{ foo bar }\n\nHere's..."
1 11 mystuff | group ()
2 11 P_STRING | "\n "
2 12 P_VALUE | pants ()
2 12 P_STRING | "\n "
2 13 P_BLOCK | morestuff ()
3 13 P_VALUE | pants ()
2 13 P_STRING | "\n"
注意:如果您从CLI运行\Tempe\Helper::dumpNode()
,您将在输出中获得花哨的格式化。实际上,这相当不错,我最初后悔浪费了写它的宝贵时间,但它已被证明非常有价值。
完全自定义的语言
如果您不喜欢、不需要或不需要Tempe\Lang\Basic
提供的内容?没问题!只需实现自己的Tempe\Lang
即可
<?php
class MyLang implements \Tempe\Lang
{
function check($handler, $node, $chainPos)
{
return true;
}
function handle($handler, $in, \Tempe\HandlerContext $context)
{
switch ($handler->name) {
case 'foo': return "foo "; break;
case 'bar': return "bar "; break;
default: return $handler->name."(".implode(", ", $handler->args).") ";
}
}
function handleEmpty(\Tempe\HandlerContext $context)
{
return "<empty>";
}
}
$lang = new MyLang();
$renderer = new \Tempe\Renderer($lang);
echo $renderer->render("{= foo }{= bar }{= baz qux }{=}");
输出
foo bar baz(qux) <empty>