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://composer.php.ac.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 了解开发环境设置。
