zordius / lightncandy
handlebars ( https://handlebars.node.org.cn/ ) 和 mustache ( http://mustache.github.io/ ) 的极快 PHP 实现。
Requires
- php: >=7.1.0
Requires (Dev)
- phpunit/phpunit: >=7
This package is auto-updated.
Last update: 2024-09-22 05:51:52 UTC
README
⚡🍭 handlebars ( https://handlebars.node.org.cn/ ) 和 mustache ( http://mustache.github.io/ ) 的极快 PHP 实现。
CI 状态: 测试 PHP 版本:7.1, 7.2, 7.3, 7.4, 8.0, 8.1
特性
- 无逻辑模板:mustache ( http://mustache.github.com/ ) 或 handlebars ( https://handlebars.node.org.cn/ ) .
- 将模板编译成 纯 PHP 代码。示例
- FAST!
- 比 mustache.php (Justin Hileman/bobthecow 实现) 快 2~7 倍。
- 比 mustache-php (Dave Ingram 实现) 快 2~7 倍。
- 比 handlebars.php 快 10~50 倍。
- 详细性能测试报告可以在 这里 找到,去 http://zordius.github.io/HandlebarsTest/ 查看图表。
- SMALL! 所有 PHP 文件大小为 189K
- ROBUST!
- 100% 支持 mustache 规范 v1.1.3。对于可选的 lambda 模块,支持 4 个 10 个规范。
- 几乎支持所有 handlebars.js 规范
- 输出与 handlebars.js 相同
- FLEXIBLE!
- 许多 选项 可以更改特性和行为。
- 上下文生成
- 从您的模板中分析使用到的功能(执行
LightnCandy::getContext()
获取)。
- 从您的模板中分析使用到的功能(执行
- 调试
- 生成调试版本的模板
- 在渲染模板时找出缺失的数据。
- 生成可视化调试模板。
- 生成调试版本的模板
- 独立模板
- 编译后的PHP代码可以在不使用任何PHP库的情况下运行。执行渲染函数时不需要包含LightnCandy。
安装
使用Composer( https://getcomposer.org.cn/ )安装LightnCandy
composer require zordius/lightncandy:dev-master
升级通知
- 请查看HISTORY.md以获取版本历史。
- 请查看UPGRADE.md以获取升级通知。
文档
编译选项
您可以通过运行 LightnCandy::compile($template, $options)
应用更多选项。
LightnCandy::compile($template, array( 'flags' => LightnCandy::FLAG_ERROR_LOG | LightnCandy::FLAG_STANDALONEPHP ));
默认情况下,将模板编译为PHP,这可以尽可能快地运行(标志 = FLAG_BESTPERFORMANCE)。
错误处理
JavaScript 兼容性
Mustache 兼容性
FLAG_MUSTACHELOOKUP
: 使递归查找行为与mustache规范对齐。但是,渲染性能将更差。FLAG_MUSTACHELAMBDA
: 支持简单的lambda逻辑,如mustache规范。但是,渲染性能将更差。FLAG_NOHBHELPERS
: 不编译handlebars.js内置助手。使用此选项时,{{#with}}
、{{#if}}
、{{#unless}}
、{{#each}}
表示普通部分,而{{#with foo}}
、{{#if foo}}
、{{#unless foo}}
、{{#each foo}}
将导致编译错误。FLAG_MUSTACHESECTION
: 使部分上下文行为与mustache.js对齐。FLAG_MUSTACHE
: 支持所有mustache规范,但性能会下降,与FLAG_ERROR_SKIPPARTIAL
+FLAG_MUSTACHELOOKUP
+FLAG_MUSTACHELAMBDA
+FLAG_NOHBHELPERS
+FLAG_RUNTIMEPARTIAL
+FLAG_JSTRUE
+FLAG_JSOBJECT
相同。
Handlebars 兼容性
- FLAG_THIS
- FLAG_PARENT
- FLAG_HBESCAPE
- FLAG_ADVARNAME
- FLAG_NAMEDARG
- FLAG_SLASH
- FLAG_ELSE
FLAG_RAWBLOCK
: 支持{{{{raw_block}}}}any_char_or_{{foo}}_as_raw_string{{{{/raw_block}}}}
。FLAG_HANDLEBARSLAMBDA
: 支持lambda逻辑,如handlebars.js规范。但是,渲染性能将更差。FLAG_SPVARS
: 支持特殊变量,包括 @root、@index、@key、@first、@last。否则,使用默认解析逻辑编译这些变量名称。FLAG_HANDLEBARS
: 支持大多数handlebars扩展,同时保持良好的性能,与FLAG_THIS
+FLAG_PARENT
+FLAG_HBESCAPE
+FLAG_ADVARNAME
+FLAG_NAMEDARG
+FLAG_SPVARS
+FLAG_SLASH
+FLAG_ELSE
+FLAG_RAWBLOCK
相同。FLAG_HANDLEBARSJS
: 支持大多数handlebars.js + JavaScript行为,同时保持良好的性能,与FLAG_JS
+FLAG_HANDLEBARS
相同。FLAG_HANDLEBARSJS_FULL
: 启用所有支持的handlebars.js行为,但性能会下降,与FLAG_HANDLEBARSJS
+FLAG_INSTANCE
+FLAG_RUNTIMEPARTIAL
+FLAG_MUSTACHELOOKUP
+FLAG_HANDLEBARSLAMBDA
相同。
Handlebars 选项
- FLAG_NOESCAPE
- FLAG_PARTIALNEWCONTEXT
FLAG_IGNORESTANDALONE
: 防止在{{#foo}}
、{{/foo}}
或{{^}}
上检测独立项,行为与handlebars.js编译时忽略独立项选项相同。FLAG_STRINGPARAMS
: 将变量名称作为字符串传递给助手,行为与handlebars.js编译时字符串参数选项相同。FLAG_KNOWNHELPERSONLY
: 仅将当前上下文传递给lambda,行为与handlebars.js编译时仅传递已知助手选项相同。FLAG_PREVENTINDENT
: 将部分缩进行为与mustache规范保持一致。这与handlebars.js的preventIndent编译时间选项相同。
PHP
- FLAG_STANDALONEPHP
- FLAG_EXTHELPER
- FLAG_RUNTIMEPARTIAL
- FLAG_PROPERTY
- FLAG_METHOD
- FLAG_INSTANCE
- FLAG_ECHO
- FLAG_BESTPERFORMANCE
部分支持
自定义助手
自定义助手示例
#mywith (上下文变化)
- LightnCandy
// LightnCandy sample, #mywith works same with #with $php = LightnCandy::compile($template, array( 'flags' => LightnCandy::FLAG_HANDLEBARSJS, 'helpers' => array( 'mywith' => function ($context, $options) { return $options['fn']($context); } ) ));
- Handlebars.js
// Handlebars.js sample, #mywith works same with #with Handlebars.registerHelper('mywith', function(context, options) { return options.fn(context); });
#myeach (上下文变化)
- LightnCandy
// LightnCandy sample, #myeach works same with #each $php = LightnCandy::compile($template, array( 'flags' => LightnCandy::FLAG_HANDLEBARSJS, 'helpers' => array( 'myeach' => function ($context, $options) { $ret = ''; foreach ($context as $cx) { $ret .= $options['fn']($cx); } return $ret; } ) ));
- Handlebars.js
// Handlebars.js sample, #myeach works same with #each Handlebars.registerHelper('myeach', function(context, options) { var ret = '', i, j = context.length; for (i = 0; i < j; i++) { ret = ret + options.fn(context[i]); } return ret; });
#myif (无上下文变化)
- LightnCandy
// LightnCandy sample, #myif works same with #if $php = LightnCandy::compile($template, array( 'flags' => LightnCandy::FLAG_HANDLEBARSJS, 'helpers' => array( 'myif' => function ($conditional, $options) { if ($conditional) { return $options['fn'](); } else { return $options['inverse'](); } } ) ));
- Handlebars.js
// Handlebars.js sample, #myif works same with #if Handlebars.registerHelper('myif', function(conditional, options) { if (conditional) { return options.fn(this); } else { return options.inverse(this); } });
您可以使用isset($options['fn'])
来检测您的自定义助手是否为块;您还可以使用isset($options['inverse'])
来检测{{else}}
的存在。
数据变量和上下文
您可以从$options['data']
中获取特殊的数据变量。使用$options['_this']
接收当前上下文。
$php = LightnCandy::compile($template, array( 'flags' => LightnCandy::FLAG_HANDLEBARSJS, 'helpers' => array( 'getRoot' => function ($options) { print_r($options['_this']); // dump current context return $options['data']['root']; // same as {{@root}} } ) ));
- Handlebars.js
Handlebars.registerHelper('getRoot', function(options) { console.log(this); // dump current context return options.data.root; // same as {{@root}} });
私有变量
当您使用第二个参数执行子块时,您可以向内部块注入私有变量。示例代码显示与{{#each}}
类似的行为,它为子块设置索引,并可以使用{{@index}}
访问。
- LightnCandy
$php = LightnCandy::compile($template, array( 'flags' => LightnCandy::FLAG_HANDLEBARSJS, 'helpers' => array( 'list' => function ($context, $options) { $out = ''; $data = $options['data']; foreach ($context as $idx => $cx) { $data['index'] = $idx; $out .= $options['fn']($cx, array('data' => $data)); } return $out; } ) ));
- Handlebars.js
Handlebars.registerHelper('list', function(context, options) { var out = ''; var data = options.data ? Handlebars.createFrame(options.data) : undefined; for (var i=0; i<context.length; i++) { if (data) { data.index = i; } out += options.fn(context[i], {data: data}); } return out; });
更改定界符
您可以将定界符从{{
和}}
更改为其他字符串。在模板中,您可以使用{{=<% %>=}}
将定界符更改为<%
和%>
,但更改不会影响包含的部分。
如果您想更改模板及其所有包含的部分的默认定界符,您可以在compile()
中使用delimiters
选项。
LightnCandy::compile('I wanna use <% foo %> as delimiters!', array( 'delimiters' => array('<%', '%>') ));
模板调试
当模板发生错误时,LightnCandy::compile()将返回false。您可以使用FLAG_ERROR_LOG
编译以查看更多错误信息,或使用FLAG_ERROR_EXCEPTION
编译以捕获异常。
当编译时,您可以使用FLAG_RENDER_DEBUG
生成模板的调试版本。调试模板包含更多调试信息且速度较慢(待定:性能结果),您可以在render函数中传递额外的LightnCandy\Runtime选项以获取更多信息渲染错误(缺少数据)。例如
$template = "Hello! {{name}} is {{gender}}. Test1: {{@root.name}} Test2: {{@root.gender}} Test3: {{../test3}} Test4: {{../../test4}} Test5: {{../../.}} Test6: {{../../[test'6]}} {{#each .}} each Value: {{.}} {{/each}} {{#.}} section Value: {{.}} {{/.}} {{#if .}}IF OK!{{/if}} {{#unless .}}Unless not OK!{{/unless}} "; // compile to debug version $phpStr = LightnCandy::compile($template, array( 'flags' => LightnCandy::FLAG_RENDER_DEBUG | LightnCandy::FLAG_HANDLEBARSJS )); // Save the compiled PHP code into a php file file_put_contents('render.php', '<?php ' . $phpStr . '?>'); // Get the render function from the php file $renderer = include('render.php'); // error_log() when missing data: // LightnCandy\Runtime: [gender] is not exist // LightnCandy\Runtime: ../[test] is not exist $renderer(array('name' => 'John'), array('debug' => LightnCandy\Runtime::DEBUG_ERROR_LOG)); // Output visual debug template with ANSI color: echo $renderer(array('name' => 'John'), array('debug' => LightnCandy\Runtime::DEBUG_TAGS_ANSI)); // Output debug template with HTML comments: echo $renderer(array('name' => 'John'), array('debug' => LightnCandy\Runtime::DEBUG_TAGS_HTML));
ANSI输出将是
以下是LightnCandy\Runtime调试选项列表,用于render函数
DEBUG_ERROR_LOG
: 当缺少所需数据时调用error_log()DEBUG_ERROR_EXCEPTION
: 当缺少所需数据时抛出异常DEBUG_TAGS
: 将render函数的返回值转换为标准化的mustache标签DEBUG_TAGS_ANSI
: 将render函数的返回值转换为带ANSI颜色的标准化mustache标签DEBUG_TAGS_HTML
: 将render函数的返回值转换为带HTML注释的标准化mustache标签
预处理部分
如果您想在部分编译之前执行额外处理,您可以在compile()
时使用prepartial
。例如,此示例通过名称标识部分添加HTML注释
$php = LightnCandy::compile($template, array( 'flags' => LightnCandy::FLAG_HANDLEBARSJS, 'prepartial' => function ($context, $template, $name) { return "<!-- partial start: $name -->$template<!-- partial end: $name -->"; } ));
您还可以通过覆盖prePartial()
静态方法来扩展LightnCandy\Partial,将您的预处理转换为内置功能。
自定义渲染函数
如果您想在渲染函数内部执行额外任务或添加更多注释,您可以在compile()
时使用renderex
。例如,此示例将编译时间注释嵌入到模板中
$php = LightnCandy::compile($template, array( 'flags' => LightnCandy::FLAG_HANDLEBARSJS, 'renderex' => '// Compiled at ' . date('Y-m-d h:i:s') ));
您的渲染函数将是
function ($in) { $cx = array(...); // compiled at 1999-12-31 00:00:00 return ..... }
请确保传入的renderex
是有效的PHP,LightnCandy不会进行检查。
自定义渲染运行时类
如果您想扩展LightnCandy\Runtime
类并替换默认的运行时库,您可以在compile()
时使用runtime
。例如,此示例将基于您的扩展MyRunTime
生成渲染函数
// Customized runtime library to debug {{{foo}}} class MyRunTime extends LightnCandy\Runtime { public static function raw($cx, $v) { return '[[DEBUG:raw()=>' . var_export($v, true) . ']]'; } } // Use MyRunTime as runtime library $php = LightnCandy::compile($template, array( 'flags' => LightnCandy::FLAG_HANDLEBARSJS, 'runtime' => 'MyRunTime' ));
请确保在编译()或根据您的FLAG_STANDALONEPHP
渲染时MyRunTime
存在。
不支持的特性
{{foo/bar}}
样式的变量名,这在官方handlebars.js文档中已被弃用,请使用以下样式:{{foo.bar}}
。
建议的Handlebars模板实践
- 避免使用
{{#with}}
。我认为{{path.to.val}}
比{{#with path.to}}{{val}}{{/with}}
更易读;使用{{#with}}
时,您会在作用域变化时感到困惑。{{#with}}
在访问同一路径下的多个变量时可以节省您很少的时间,但理解和维护模板时却会花费您很多时间。 - 当您不需要对值进行HTML转义输出时,请使用
{{{val}}}
。它也有更好的性能。 - 如果您想在不同的语言中重用模板,请避免使用自定义助手。或者,您可能需要在不同的语言中实现不同版本的助手。
- 为了获得最佳性能,您应该在开发阶段仅使用“按需编译”模式。在您进入生产阶段之前,可以对所有模板进行
LightnCandy::compile()
,保存所有生成的PHP代码,并部署这些生成的文件(您可能需要维护一个构建过程)。请勿在生产环境中编译,这同样是安全性的最佳实践。为“按需编译”添加缓存不是最佳解决方案。如果您想基于LightnCandy构建一些库或框架,请考虑这个场景。 - 每次升级LightnCandy时,都重新编译您的模板。
- Handlebars和lightncandy对
{
或}
的持久转义实践- 如果您想显示原子
}}
,您可以直接使用它而无需任何技巧。例如:{{foo}} }}
- 如果您想在任何handlebars令牌后立即显示
}
,您可以使用这个:{{#with "}"}}{{.}}{{/with}}
。例如:{{foo}}{{#with "}"}}{{.}}{{/with}}
- 如果您想显示原子
{
,您可以直接使用它而无需任何技巧。例如:{ and {{foo}}
。 - 如果您想显示
{{
,您可以使用{{#with "{{"}}{{.}}{{/with}}
。例如:{{#with "{{"}}{{.}}{{/with}}{{foo}}
- 如果您想显示原子
详细功能列表
访问https://handlebars.node.org.cn/以查看关于handlebars.js的更多功能描述。所有功能都与它一致。
- 与handlebars.js相同的CR/LF行为
- 与mustache规范相同的CR/LF行为
- 与handlebars.js相同的'true'或'false'输出(需要
FLAG_JSTRUE
) - 与handlebars.js相同的'[object Object]'输出或join(',' array)输出(需要
FLAG_JSOBJECT
) - 可以在
{{ var }}
或{{{ var }}}
内部放置标题/尾部空格、制表符、CR/LF - 部分缩进行为与mustache规范相同
- 递归变量查找到父上下文的行为与mustache规范相同(需要
FLAG_MUSTACHELOOKUP
) {{{value}}}
或{{&value}}
:原始变量- 作为'true'的true(需要
FLAG_JSTRUE
) - 作为'false'的false(需要
FLAG_TRUE
)
- 作为'true'的true(需要
{{value}}
:HTML转义变量- 作为'true'的true(需要
FLAG_JSTRUE
) - 作为'false'的false(需要
FLAG_JSTRUE
)
- 作为'true'的true(需要
{{{path.to.value}}}
:点表示法,原始{{path.to.value}}
:点表示法,HTML转义{{.}}
:当前上下文,HTML转义{{{.}}}
:当前上下文,原始{{this}}
:当前上下文,HTML转义(需要FLAG_THIS
){{{this}}}
:当前上下文,原始(需要FLAG_THIS
){{#value}}
:部分- false、undefined和null将跳过部分
- true将以原始作用域运行部分
- 所有其他部分将以新作用域运行(包括0、1、-1、''、'1'、'0'、'-1'、'false'、Array等)
{{/value}}
: 结束部分{{^value}}
: 逆序部分- 布尔值、未定义和空值将使用原始作用域运行该部分
- 所有其他情况都将跳过该部分(包括0、1、-1、''、'1'、'0'、'-1'、'false'、Array等)
{{! comment}}
: 注释{{!-- comment or {{ or }} --}}
: 扩展注释,可以包含}}或{{ .{{=<% %>=}}
: 设置自定义分隔符,自定义字符串不能包含=
。更多示例请查看http://mustache.github.io/mustache.5.html。{{#each var}}
: 每次循环{{#each}}
: 在{{.}}上执行循环{{/each}}
: 结束循环{{#each bar as |foo|}}
: 在bar上执行循环,并将值设置为foo。 (需要FLAG_ADVARNAME
){{#each bar as |foo moo|}}
: 在bar上执行循环,将值设置为foo,将索引设置为moo。 (需要FLAG_ADVARNAME
){{#if var}}
: 使用原始作用域执行if逻辑(null、false、空数组和''将跳过此块){{#if foo includeZero=true}}
: 当foo === 0时结果为true(需要FLAG_NAMEDARG
){{/if}}
: 结束if{{else}}
或{{^}}
: 执行else逻辑,应在{{#if var}}
和{{/if}}
之间;或在{{#unless var}}
和{{/unless}}
之间;或在{{#foo}}
和{{/foo}}
之间;或在{{#each var}}
和{{/each}}
之间;或在{{#with var}}
和{{/with}}
之间。(需要FLAG_ELSE
){{#if foo}} ... {{else if bar}} ... {{/if}}
: 连接的if else块{{#unless var}}
: 使用原始作用域执行unless逻辑(null、false、空数组和''将渲染此块){{#unless foo}} ... {{else if bar}} ... {{/unless}}
: 连接的unless else块{{#unless foo}} ... {{else unless bar}} ... {{/unless}}
: 连接的unless else块{{#foo}} ... {{else bar}} ... {{/foo}}
: 自定义辅助器连接的else块{{#with var}}
: 改变上下文作用域。如果var是false或空数组,则跳过包含的部分。{{#with bar as |foo|}}
: 将上下文更改为bar,并将值设置为foo。 (需要FLAG_ADVARNAME
){{lookup foo bar}}
: 通过bar的值作为键查找foo。{{../var}}
: 父模板作用域。(需要FLAG_PARENT
){{>file}}
: 部分内容;在模板内包含另一个模板。{{>file foo}}
: 具有新上下文的部分(需要FLAG_RUNTIMEPARTIAL
){{>file foo bar=another}}
: 混合了后续键值的新上下文部分(需要FLAG_RUNTIMEPARTIAL
){{>(helper) foo}}
: 通过从助手提供名称包含动态部分(需要FLAG_RUNTIMEPARTIAL
){{@index}}
: 在数组上的{{#each}}
循环中的当前索引的引用。(需要FLAG_SPVARS
){{@key}}
: 在对象上的{{#each}}
循环中的当前键的引用。(需要FLAG_SPVARS
){{@root}}
: 根上下文的引用。(需要FLAG_SPVARS
){{@first}}
: 当循环第一个项目时为true。(需要FLAG_SPVARS
){{@last}}
: 当循环最后一个项目时为true。(需要FLAG_SPVARS
){{@root.path.to.value}}
: 沿着路径跟踪根上下文。(需要FLAG_SPVARS
){{@../index}}
: 访问父循环索引。(需要FLAG_SPVARS
和FLAG_PARENT
){{@../key}}
: 访问父循环键。(需要FLAG_SPVARS
和FLAG_PARENT
){{foo.[ba.r].[#spec].0.ok}}
: 引用$CurrentConext['foo']['ba.r']['#spec'][0]['ok']。(需要FLAG_ADVARNAME
){{~any_valid_tag}}
: 空格控制,删除所有之前的间隔(包括CR/LF、制表符、空格;在遇到任何非间隔字符时停止){{any_valid_tag~}}
: 空格控制,移除所有后续空格(包括回车换行、制表符、空格;遇到非空格字符停止){{{helper var}}}
: 执行自定义助手并渲染结果{{helper var}}
: 执行自定义助手并渲染转义后的HTML结果{{helper "str"}}
或{{helper 'str'}}
: 使用字符串参数执行自定义助手(需要FLAG_ADVARNAME
){{helper 123 null true false undefined}}
: 将数字、true、false、null或undefined传递给助手{{helper name1=var name2=var2}}
: 使用命名参数执行自定义助手(需要FLAG_NAMEDARG
){{#helper ...}}...{{/helper}}
: 执行块自定义助手{{helper (helper2 foo) bar}}
: 将自定义助手作为子表达式执行(需要FLAG_ADVARNAME
){{{{raw_block}}}} {{will_not_parsed}} {{{{/raw_block}}}}
: 原始块(需要FLAG_RAWBLOCK
){{#> foo}}block{{/foo}}
: 部分块,提供foo
部分默认内容(需要FLAG_RUNTIMEPARTIAL
){{#> @partial-block}}
: 在部分中访问部分块内容{{#*inline "partial_name"}}...{{/inline}}
: 内联部分,提供部分并覆盖原始部分。{{log foo}}
: 将值输出到stderr进行调试。
开发者笔记
请阅读 CONTRIBUTING.md 了解开发环境设置。