zordius/lightncandy

handlebars ( https://handlebars.node.org.cn/ ) 和 mustache ( http://mustache.github.io/ ) 的极快 PHP 实现。

v1.2.6 2021-07-11 04: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 覆盖率状态

Packagist 上的包:最新稳定版本 许可证 总下载次数

特性

安装

使用Composer( https://getcomposer.org.cn/ )安装LightnCandy

composer require zordius/lightncandy:dev-master

升级通知

文档

编译选项

您可以通过运行 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

部分支持

自定义助手

自定义助手示例

#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
  • {{value}}:HTML转义变量
    • 作为'true'的true(需要FLAG_JSTRUE
    • 作为'false'的false(需要FLAG_JSTRUE
  • {{{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_SPVARSFLAG_PARENT
  • {{@../key}} : 访问父循环键。(需要FLAG_SPVARSFLAG_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 了解开发环境设置。

框架集成

工具