zordius/lightncandy

An extremely fast PHP implementation of handlebars ( http://handlebarsjs.com/ ) and mustache ( http://mustache.github.io/ ).

Installs: 116 429

Dependents: 18

Stars: 269

Watchers: 26

Forks: 41

Open Issues: 6

V0.91 2016-02-03 10:39 UTC

README

⚡🍭 An extremely fast PHP implementation of handlebars ( http://handlebarsjs.com/ ) and mustache ( http://mustache.github.io/ ).

Travis CI status: Unit testing Regression testing

Scrutinizer CI status: Code Coverage

Package on packagist: Latest Stable Version License Total Downloads HHVM Status

Features

Installation

Use Composer ( https://getcomposer.org/ ) to install LightnCandy:

composer require zordius/lightncandy:dev-master

UPGRADE NOTICE

Documents

Compile Options

You can apply more options by running LightnCandy::compile($template, $options):

LightnCandy::compile($template, Array(
    'flags' => LightnCandy::FLAG_ERROR_LOG | LightnCandy::FLAG_STANDALONEPHP
));

Default is to compile the template as PHP, which can be run as fast as possible (flags = FLAG_BESTPERFORMANCE).

  • FLAG_ERROR_LOG
  • FLAG_ERROR_EXCEPTION
  • FLAG_ERROR_SKIPPARTIAL
  • FLAG_NOESCAPE
  • FLAG_STANDALONEPHP
  • FLAG_JSTRUE
  • FLAG_JSOBJECT
  • FLAG_THIS
  • FLAG_PARENT
  • FLAG_HBESCAPE
  • FLAG_ADVARNAME
  • FLAG_NAMEDARG
  • FLAG_EXTHELPER : do not including custom helper codes into compiled PHP codes. This reduces the code size, but you need to take care of your helper functions when rendering. If you forget to include required functions when execute rendering function, undefined function runtime error will be triggered. NOTE: Anonymous functions will always be placed into generated codes.
  • FLAG_RUNTIMEPARTIAL
  • FLAG_PARTIALNEWCONTEXT : create a new context for the partial, the behavior is same with handlebars.js explicitPartialContext compile time option.
  • FLAG_SLASH : Skip a delimiter when it behind \ .
  • FLAG_ELSE : support {{else}} or {{^}} as handlebars specification. Otherwise, {{else}} will be resolved as normal variable , and {{^}} will cause template error.
  • FLAG_RAWBLOCK: support {{{{raw_block}}}} any char or {{foo}} as none parsed raw string {{{{/raw_block}}}}.
  • FLAG_PROPERTY : support object instance attribute access. You MUST apply this if your data contains object. And, the rendering performance will be worse.
  • FLAG_METHOD : support object instance method access. You MUST apply this if your data contains object. And, the rendering performance will be worse.
  • FLAG_INSTANCE : same with FLAG_PROPERTY + FLAG_METHOD
  • FLAG_SPACECTL : support space control {{~ }} or {{ ~}} in template. Otherwise, {{~ }} or {{ ~}} will cause template error.
  • FLAG_IGNORESTANDALONE : prevent standalone detection on {{#foo}}, {{/foo}} or {{^}}, the behavior is same with handlebars.js ignoreStandalone compile time option.
  • FLAG_STRINGPARAMS : pass variable name as string to helpers, the behavior is same with handlebars.js stringParams compile time option.
  • FLAG_KNOWNHELPERSONLY: Only pass current context to lambda, the behavior is same with handlebars.js knownHelpersOnly compile time option.
  • FLAG_SPVARS : support special variables include @root, @index, @key, @first, @last. Otherwise, compile these variable names with default parsing logic.
  • FLAG_HANDLEBARSLAMBDA : support lambda logic as handlebars.js specification. And, the rendering performance will be worse.
  • FLAG_JS : simulate all JavaScript string conversion behavior, same with FLAG_JSTRUE + FLAG_JSOBJECT.
  • FLAG_HANDLEBARS : support most handlebars extensions and also keep performance good, same with FLAG_THIS + FLAG_PARENT + FLAG_HBESCAPE + FLAG_ADVARNAME + FLAG_SPACECTL + FLAG_NAMEDARG + FLAG_SPVARS + FLAG_SLASH + FLAG_ELSE + FLAG_RAWBLOCK.
  • FLAG_HANDLEBARSJS : support most handlebars.js + javascript behaviors and also keep performance good, same with FLAG_JS + FLAG_HANDLEBARS.
  • FLAG_HANDLEBARSJS_FULL : enable all supported handlebars.js behaviors but performance drop, same with FLAG_HANDLEBARSJS + FLAG_INSTANCE + FLAG_RUNTIMEPARTIAL + FLAG_MUSTACHELOOKUP + FLAG_HANDLEBARSLAMBDA.
  • FLAG_MUSTACHELOOKUP : align recursive lookup up behaviors with mustache specification. And, the rendering performance will be worse.
  • FLAG_MUSTACHELAMBDA : support simple lambda logic as mustache specification. And, the rendering performance will be worse.
  • FLAG_PREVENTINDENT : align partial indent behavior with mustache specification. This is same with handlebars.js preventIndent copmile time option.
  • FLAG_NOHBHELPERS : Do not compile handlebars.js builtin helpers. With this option, {{#with}}, {{#if}}, {{#unless}}, {{#each}} means normal section, and {{#with foo}}, {{#if foo}}, {{#unless foo}}, {{#each foo}} will cause compile error.
  • FLAG_MUSTACHE : support all mustache specification but performance drop, same with FLAG_ERROR_SKIPPARTIAL + FLAG_MUSTACHELOOKUP + FLAG_MUSTACHELAMBDA + FLAG_NOHBHELPERS + FLAG_RUNTIMEPARTIAL + FLAG_JS.
  • FLAG_ECHO : compile to echo 'a', $b, 'c'; to improve performance. This will slow down rendering when the template and data are simple, but will improve 5% ~ 10% when the data is big and looping in the template.
  • FLAG_BESTPERFORMANCE : same with FLAG_ECHO + FLAG_STANDALONEPHP now. This flag may be changed base on performance testing result in the future.
  • FLAG_RENDER_DEBUG : generate debug template to show error when rendering. With this flag, the performance of rendering may be slowed.

Partial Support

You can use partialresolver option to create your own partial loader:

LightnCandy::compile($template, Array(
    'partialresolver' => function ($context, $name) {
        return MyPartialLoader($name); // Return partial content
    }
));

Dynamic Partial

You can use dynamic partial name by passing a custom helper as subexpression syntax, for example: {{> (foo)}} . the return value of custom helper foo will be the partial name.

$php = LightnCandy::compile('{{> (partial_name_helper obj_type)}}', Array(
    'flags' => LightnCandy::FLAG_HANDLEBARSJS | LightnCandy::FLAG_RUNTIMEPARTIAL,
    'helpers' => Array(
        'partial_name_helper' => function ($args) {
            switch ($args[0]) {
                ....
            }
        }
    ),
    'partials' => Array(
        'people' => 'This is {{name}}, he is {{age}} years old.',
        'animal' => 'This is {{name}}, it is {{age}} years old.',
    )
));

$renderer = LightnCandy::prepare($php);

// Will use people partial and output: 'This is John, he is 15 years old.'
echo $renderer(Array(
   'obj_type' => 'people',
   'name' => 'John',
   'age' => '15',
));

When you using dynamic partial, LightnCandy will compile all partials inside the partials option into template. This makes the generated code larger, but this can make sure all partials are included for rendering. (TODO: add an example to show how to provide partials across templates to reduce size)

Custom Helper

Custom Helper Escaping

The return value of your custom helper should be a string. When your custom helper be executed from {{ }} , the return value will be HTML escaped. You may execute your helper by {{{ }}} , then the original helper return value will be outputted directly.

If you return a LightnCandy\SafeString object, it will not be html escaped.

// escaping is handled by lightncandy and decided by template
// if the helper is in {{ }} , you get 'The U&ME Helper is ececuted!'
// if the helper is in {{{ }}} , you get 'The U&ME Helper is executed!'
return 'The U&ME Helper is executed!';

// Do not escape anything.
// No matter in {{ }} or {{{ }}} , you get 'Exact&Same output \' \" Ya!'
return new LightnCandy\SafeString('Exact&Same output \' " Ya!');

// Force to escape the result.
// No matter in {{ }} or {{{ }}} , you get 'Not&Same output ' " Ya!'
return new LightnCandy\SafeString('Not&Same output \' " Ya!', true);

// Force to escape the result in handlebars.js way
// No matter in {{ }} or {{{ }}} , you get 'Not&Same output ' " Ya!'
return new LightnCandy\SafeString('Not&Same output \' " Ya!', 'encq');

Custom Helper Examples

#mywith (context change)

  • 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 (context change)

  • 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 (no context change)

  • 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);
    }
});

You can use isset($options['fn']) to detect your custom helper is a block or not; you can also use isset($options['inverse']) to detect the existence of {{else}}.

Hashed arguments

  • LightnCandy
$php = LightnCandy::compile($template, Array(
    'flags' => LightnCandy::FLAG_HANDLEBARSJS,
    'helpers' => Array(
        'sample' => function ($arg1, $arg2, $options) {
            // All hashed arguments are in $options['hash']
        }
    )
));
  • Handlebars.js
Handlebars.registerHelper('sample', function(arg1, arg2, options) {
    // All hashed arguments are in options.hash
});

Data variables and context

You can get special data variables from $options['data']. Using $options['_this'] to receive current context.

$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}}
});

Private variables

You can inject private variables into inner block when you execute child block with second parameter. The example code showed similar behavior with {{#each}} which sets index for child block and can be accessed with {{@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;
});

Escaping

When a Handlebars.js style custom helper be used as block tags, LightnCandy will not escape the result. When it is a single {{...}} tag, LightnCandy will escape the result. To change the escape behavior, you can return extended information by Array(), please read Custom Helper Escaping for more.

Change Delimiters

You may change delimiters from {{ and }} to other strings. In the template, you can use {{=<% %>=}} to change delimiters to <% and %> , but the change will not affect included partials.

If you want to change default delimiters for a template and all included partials, you may compile() it with delimiters option:

LightnCandy::compile('I wanna use <% foo %> as delimiters!', Array(
    'delimiters' => array('<%', '%>')
));

Template Debugging

When template error happened, LightnCandy::compile() will return false. You may compile with FLAG_ERROR_LOG to see more error message, or compile with FLAG_ERROR_EXCEPTION to catch the exception.

You may generate debug version of templates with FLAG_RENDER_DEBUG when compile() . The debug template contained more debug information and slower (TBD: performance result) , you may pass extra LightnCandy\Runtime options into render function to know more rendering error (missing data). For example:

$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
$php = LightnCandy::compile($template, Array(
    'flags' => LightnCandy::FLAG_RENDER_DEBUG | LightnCandy::FLAG_HANDLEBARSJS
));

// Get the render function
$renderer = LightnCandy::prepare($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));

The ANSI output will be:

example_debug.png

Here are the list of LightnCandy\Runtime debug options for render function:

  • DEBUG_ERROR_LOG : error_log() when missing required data
  • DEBUG_ERROR_EXCEPTION : throw exception when missing required data
  • DEBUG_TAGS : turn the return value of render function into normalized mustache tags
  • DEBUG_TAGS_ANSI : turn the return value of render function into normalized mustache tags with ANSI color
  • DEBUG_TAGS_HTML : turn the return value of render function into normalized mustache tags with HTML comments

Preprocess Partials

If you want to do extra process before the partial be compiled, you may use prepartial when compile(). For example, this sample adds HTML comments to identify the partial by the name:

$php = LightnCandy::compile($template, Array(
    'flags' => LightnCandy::FLAG_HANDLEBARSJS,
    'prepartial' => function ($context, $template, $name) {
        return "<!-- partial start: $name -->$template<!-- partial end: $name -->";
    }
));

You may also extend LightnCandy\Partial by override the prePartial() static method to turn your preprocess into a built-in feature.

Customize Render Function

If you want to do extra tasks inside render function or add more comment, you may use renderex when compile() . For example, this sample embed the compile time comment into the template:

$php = LightnCandy::compile($template, Array(
    'flags' => LightnCandy::FLAG_HANDLEBARSJS,
    'renderex' => '// Compiled at ' . date('Y-m-d h:i:s')
));

Your render function will be:

function ($in) {
    $cx = array(...);
    // compiled at 1999-12-31 00:00:00
    return .....
}

Please make sure the passed in renderex is valid PHP, LightnCandy will not check it.

Customize Rendering Runtime Class

If you want to extend LightnCandy\Runtime class and replace the default runtime library, you may use runtime when compile() . For example, this sample will generate render function based on your extended 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'
));

Please make sure MyRunTime exists when compile() or rendering based on your FLAG_STANDALONEPHP .

Unsupported Feature (so far)

  • [NEVER] {{foo/bar}} style variable name, it is deprecated in official handlebars.js document, please use this style: {{foo.bar}}.

Suggested Handlebars Template Practices

  • Prevent to use {{#with}} . I think {{path.to.val}} is more readable then {{#with path.to}}{{val}}{{/with}}; when using {{#with}} you will confusing on scope changing. {{#with}} only save you very little time when you access many variables under same path, but cost you a lot time when you need to understand then maintain a template.
  • use {{{val}}} when you do not require HTML escaped output on the value. It is better performance, too.
  • If you wanna display {{, use this: {{#with "{{"}}{{.}}{{/with}}.
  • Prevent to use custom helper if you want to reuse your template in different language. Or, you may need to implement different versions of helper in different languages.
  • For best performance, you should only use 'compile on demand' pattern when you are in development stage. Before you go to production, you can LightnCandy::compile() on all your templates, save all generated PHP codes, and deploy these generated files (You may need to maintain a build process for this) . DO NOT COMPILE ON PRODUCTION , it also a best practice for security. Adding cache for 'compile on demand' is not the best solution. If you want to build some library or framework based on LightnCandy, think about this scenario.
  • Recompile your templates when you upgrade LightnCandy every time.

Detail Feature list

Go http://handlebarsjs.com/ to see more feature description about handlebars.js. All features align with it.

  • Exact same CR/LF behavior with handlebars.js
  • Exact same CR/LF bahavior with mustache spec
  • Exact same 'true' or 'false' output with handlebars.js (require FLAG_JSTRUE)
  • Exact same '[object Object]' output or join(',' array) output with handlebars.js (require FLAG_JSOBJECT)
  • Can place heading/tailing space, tab, CR/LF inside {{ var }} or {{{ var }}}
  • Indent behavior of the partial same with mustache spec
  • Recursive variable lookup to parent context behavior same with mustache spec (require FLAG_MUSTACHELOOKUP)
  • {{{value}}} or {{&value}} : raw variable
    • true as 'true' (require FLAG_JSTRUE)
    • false as 'false' (require FLAG_TRUE)
  • {{value}} : HTML escaped variable
    • true as 'true' (require FLAG_JSTRUE)
    • false as 'false' (require FLAG_JSTRUE)
  • {{{path.to.value}}} : dot notation, raw
  • {{path.to.value}} : dot notation, HTML escaped
  • {{.}} : current context, HTML escaped
  • {{{.}}} : current context, raw
  • {{this}} : current context, HTML escaped (require FLAG_THIS)
  • {{{this}}} : current context, raw (require FLAG_THIS)
  • {{#value}} : section
    • false, undefined and null will skip the section
    • true will run the section with original scope
    • All others will run the section with new scope (includes 0, 1, -1, '', '1', '0', '-1', 'false', Array, ...)
  • {{/value}} : end section
  • {{^value}} : inverted section
    • false, undefined and null will run the section with original scope
    • All others will skip the section (includes 0, 1, -1, '', '1', '0', '-1', 'false', Array, ...)
  • {{! comment}} : comment
  • {{!-- comment or {{ or }} --}} : extended comment that can contain }} or {{ .
  • {{=<% %>=}} : set delimiter to custom string , the custom string can not contain = . Check http://mustache.github.io/mustache.5.html for more example.
  • {{#each var}} : each loop
  • {{#each}} : each loop on {{.}}
  • {{/each}} : end loop
  • {{#if var}} : run if logic with original scope (null, false, empty Array and '' will skip this block)
  • {{#if foo includeZero=true}} : result as true when foo === 0 (require FLAG_NAMEDARG)
  • {{/if}} : end if
  • {{else}} or {{^}} : run else logic, should between {{#if var}} and {{/if}} ; or between {{#unless var}} and {{/unless}}; or between {{#foo}} and {{/foo}}; or between {{#each var}} and {{/each}}; or between {{#with var}} and {{/with}}. (require FLAG_ELSE)
  • {{#if foo}} ... {{else if bar}} ... {{/if}} : chained if else blocks
  • {{#unless var}} : run unless logic with original scope (null, false, empty Array and '' will render this block)
  • {{#unless foo}} ... {{else if bar}} ... {{/unless}} : chained unless else blocks
  • {{#unless foo}} ... {{else unless bar}} ... {{/unless}} : chained unless else blocks
  • {{#with var}} : change context scope. If the var is false, skip included section.
  • {{#with bar as |foo|}} : change context to bar and set the value as foo. (require FLAG_ADVARNAME)
  • {{lookup foo bar}} : lookup foo by value of bar as key.
  • {{../var}} : parent template scope. (require FLAG_PARENT)
  • {{>file}} : partial; include another template inside a template.
  • {{>file foo}} : partial with new context (require FLAG_RUNTIMEPARTIAL)
  • {{>file foo bar=another}} : partial with new context which mixed with followed key value (require FLAG_RUNTIMEPARTIAL)
  • {{>(helper) foo}} : include dynamic partial by name provided from a helper (require FLAG_RUNTIMEPARTIAL)
  • {{@index}} : references to current index in a {{#each}} loop on an array. (require FLAG_SPVARS)
  • {{@key}} : references to current key in a {{#each}} loop on an object. (require FLAG_SPVARS)
  • {{@root}} : references to root context. (require FLAG_SPVARS)
  • {{@first}} : true when looping at first item. (require FLAG_SPVARS)
  • {{@last}} : true when looping at last item. (require FLAG_SPVARS)
  • {{@root.path.to.value}} : references to root context then follow the path. (require FLAG_SPVARS)
  • {{@../index}} : access to parent loop index. (require FLAG_SPVARS and FLAG_PARENT)
  • {{@../key}} : access to parent loop key. (require FLAG_SPVARS and FLAG_PARENT)
  • {{foo.[ba.r].[#spec].0.ok}} : references to $CurrentConext['foo']['ba.r']['#spec'][0]['ok'] . (require FLAG_ADVARNAME)
  • {{~any_valid_tag}} : Space control, remove all previous spacing (includes CR/LF, tab, space; stop on any none spacing character) (require FLAG_SPACECTL)
  • {{any_valid_tag~}} : Space control, remove all next spacing (includes CR/LF, tab, space; stop on any none spacing character) (require FLAG_SPACECTL)
  • {{{helper var}}} : Execute custom helper then render the result
  • {{helper var}} : Execute custom helper then render the HTML escaped result
  • {{helper "str"}} or {{helper 'str'}} : Execute custom helper with string arguments (require FLAG_ADVARNAME)
  • {{helper 123 null true false undefined}} : Pass number, true, false, null or undefined into helper
  • {{helper name1=var name2=var2}} : Execute custom helper with named arguments (require FLAG_NAMEDARG)
  • {{#helper ...}}...{{/helper}} : Execute block custom helper
  • {{helper (helper2 foo) bar}} : Execute custom helpers as subexpression (require FLAG_ADVARNAME)
  • {{{{raw_block}}}} {{will_not_parsed}} {{{{/raw_block}}}} : Raw block (require FLAG_RAWBLOCK)
  • {{#> foo}}block{{/foo}} : Partial block, provide foo partial default content (require FLAG_RUNTIMEPARTIAL)
  • {{#> @partial-block}} : access partial block content inside a partial
  • {{#*inline "partial_name"}}...{{/inline}} : Inline partial, provide a partial and overwrite the original one.
  • {{log foo}} : output value to stderr for debug.

TODO

Framework Integration

Tools