shabbyrobe/tempe

此包已被废弃且不再维护。未建议替代包。

非常简单、灵活的 PHP 模板语言

维护者

详细信息

git.sr.ht/~shabbyrobe/tempe

主页

2.0.0-beta4 2017-02-28 06:36 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!{/}

可以使用seteqvar的组合来测试复杂表达式。这允许在比较中使用连接

{# 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 &amp; 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:转换为base64
  • nl2spc:将一个或多个连续的换行符转换为单个空格
  • 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 将被设置为 hargs 将被设置为 [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。如果处理程序lonesomechainable为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>