diff options
Diffstat (limited to 'vendor/zordius/lightncandy')
28 files changed, 6176 insertions, 0 deletions
diff --git a/vendor/zordius/lightncandy/.scrutinizer.yml b/vendor/zordius/lightncandy/.scrutinizer.yml new file mode 100644 index 00000000..471470fe --- /dev/null +++ b/vendor/zordius/lightncandy/.scrutinizer.yml @@ -0,0 +1,95 @@ +filter: + excluded_paths: [tests/*, build/*] + +checks: + php: + code_rating: true + duplication: true + variable_existence: true + useless_calls: true + use_statement_alias_conflict: true + unused_variables: true + unused_properties: true + unused_parameters: true + unused_methods: true + unreachable_code: true + sql_injection_vulnerabilities: true + security_vulnerabilities: true + precedence_mistakes: true + precedence_in_conditions: true + parameter_non_unique: true + no_property_on_interface: true + no_non_implemented_abstract_methods: true + deprecated_code_usage: true + closure_use_not_conflicting: true + closure_use_modifiable: true + avoid_useless_overridden_methods: true + avoid_conflicting_incrementers: true + assignment_of_null_return: true + verify_property_names: true + verify_argument_usable_as_reference: true + use_self_instead_of_fqcn: true + uppercase_constants: true + too_many_arguments: true + switch_fallthrough_commented: true + spacing_of_function_arguments: true + spacing_around_non_conditional_operators: true + spacing_around_conditional_operators: true + space_after_cast: true + single_namespace_per_use: true + simplify_boolean_return: true + side_effects_or_types: true + scope_indentation: + spaces_per_level: '4' + return_doc_comments: true + return_doc_comment_if_not_inferrable: true + require_scope_for_properties: true + require_scope_for_methods: true + require_php_tag_first: true + require_closing_tag: true + prefer_while_loop_over_for_loop: true + prefer_unix_line_ending: true + prefer_sapi_constant: true + parameter_doc_comments: true + param_doc_comment_if_not_inferrable: true + more_specific_types_in_doc_comments: true + overriding_private_members: true + optional_parameters_at_the_end: true + non_commented_empty_catch_block: true + no_unnecessary_if: true + no_unnecessary_function_call_in_for_loop: true + no_unnecessary_final_modifier: true + lowercase_php_keywords: true + lowercase_basic_constants: true + avoid_unnecessary_concatenation: true + avoid_fixme_comments: true + avoid_duplicate_types: true + avoid_corrupting_byteorder_marks: true + argument_type_checks: true + avoid_superglobals: true + avoid_perl_style_comments: true + avoid_multiple_statements_on_same_line: true + avoid_length_functions_in_loops: true + missing_arguments: true + method_calls_on_non_object: true + no_mixed_inline_html: true + no_goto: true + no_exit: true + no_eval: true + no_error_suppression: true + no_empty_statements: true + no_elseif_statements: true + no_duplicate_arguments: true + no_debug_code: true + +tools: + external_code_coverage: true + php_code_coverage: true + php_cs_fixer: true + php_code_sniffer: true + php_cpd: true + php_hhvm: true + php_mess_detector: true + php_analyzer: true + php_pdepend: true + sensiolabs_security_checker: true diff --git a/vendor/zordius/lightncandy/CONTRIBUTING.md b/vendor/zordius/lightncandy/CONTRIBUTING.md new file mode 100644 index 00000000..0d7aa65d --- /dev/null +++ b/vendor/zordius/lightncandy/CONTRIBUTING.md @@ -0,0 +1,18 @@ +# How to Contribute + +## Reporting Issues + +Submit your issue here: https://github.com/zordius/lightncandy/issues/new + +Proper sample input data with template is prefered. If you can provide the LightnCandy version (or commit hash) and some sample code of your setup/helpers it will be better. + +## Pull Requests + +Pull request is another good way. Before you submit your patch, please ensure you run full tests: + +```sh +git submodule init +git submodule update +build/runphp build/gen_test.php +phpunit +``` diff --git a/vendor/zordius/lightncandy/HISTORY.md b/vendor/zordius/lightncandy/HISTORY.md new file mode 100644 index 00000000..577f953d --- /dev/null +++ b/vendor/zordius/lightncandy/HISTORY.md @@ -0,0 +1,183 @@ +HISTORY +======= + +master current trunk + * align with handlebars.js master + +v0.17 https://github.com/zordius/lightncandy/tree/v0.17 + * 7bcce4c1a7 support {{@last}} for {{#each}} on both object and array. + * b0c44c3b40 remove ending \n in lightncandy.php + * e130875d5a support single quoted string input: {{foo 'bar'}}. + * c603aa39d8 support `renderex` to extend anything in render function. + * f063e5302c now render function debug constants works well in standalone mode. + * 53f6a6816d fix parsing bug when there is a `=` inside single quoted string. + * 2f16c0c393 now really autoload when installed with composer. + * c4da1f576c supports {{^myHelper}}. + +v0.17 https://github.com/zordius/lightncandy/tree/v0.17 + * 3b48a0acf7 fix parsing bug when FLAG_NOESCAPE enabled + * 5c774b1b08 fix hbhelpers response error with options['fn'] when FLAG_BESTPERFORMANCE enabled + * c60fe70bdb fix hbhelpers response error with options['inverse'] when FLAG_BESTPERFORMANCE enabled + * e19b3e3426 provide options['root'] and options['_parent'] to hbhelpers + * d8a288e83b refine variable parsing logic to support {{@../index}}, {{@../key}}, etc. + +v0.16 https://github.com/zordius/lightncandy/tree/v0.16 + * align with handlebars.js 2.0.0 + * 4f036aff62 better error message for named arguments. + * 0b462a387b support {{#with var}} ... {{else}} ... {{/with}}. + * 4ca624f651 fix 1 ANSI code error. + * 01ea3e9f42 support instances with PHP __call magic funciton. + * 38059036a7 support {{#foo}} or {{#each foo}} on PHP Traversable instance. + * 366f5ec0ac add FLAG_MUSTACHESP and FLAG_MUSTACHEPAIN into FLAG_HANDLEBARS and FLAG_HANDLEBARSJS now. + * b61d7b4a81 align with handlebars.js standalone tags behavior. + * b211e1742e now render false as 'false'. + * 655a2485be fix bug for {{helper "==="}} + * bb58669162 support FLAG_NOESCAPE + +v0.15 https://github.com/zordius/lightncandy/tree/v0.15 + * align with handlebars.js 2.0.0 + * 4c750806e8 fix for \ in template + * 12ab6626d6 support escape. \{{foo}} will be rendered as is. ( handlebars spec , require FLAG_SLASH ) + * 876bd44d9c escape ` to &#x60; ( require FLAG_JSQUOTE ) + * f1f388ed79 support {{^}} as {{else}} ( require FLAG_ELSE ) + * d5e17204b6 support {{#each}} == {{#each .}} now. + * 742126b440 fix {{>foo/bar}} partial not found bug. + * d62c261ff9 support numbers as helper input {{helper 0.1 -1.2}} + * d40c76b84f support escape in string arguments {{helper "test \" double quote"}} + * ecb57a2348 fix for missing partial in partial bug. + * 1adad5dbfa fix {{#with}} error when FLAG_WITH not used. + * ffd5e35c2d fix error when rendering array value as {{.}} without FLAG_JSOBJECT. + * bd4987adbd support changing context on partial {{>foo bar}} ( require FLAG_RUNTIMEPARTIAL ) + * f5decaa7e3 support name sarguments on partial {{>foo bar name=tee}} . fix {{..}} bug. + * c20bb36457 support `partials` in options. + * e8779dbe8c change default `basedir` hehavior, stop partial files lookup when do not prodive `basedir` in options. + * c4e3401fe4 fix {{>"test"}} or {{>[test]}} or {{>1234}} bug. + * e59f62ea9b fix seciton behavior when input is object, and add one new flag: FLAG_MUSTACHESEC. + * 80eaf8e007 use static::method not self::method for subclass. + * 0bad5c8f20 fix usedFeature generation bugs + +v0.14 https://github.com/zordius/lightncandy/tree/v0.14 + * align with handlebars.js 2.0.0-alpha.4 + * fa6225f278 support boolen value in named arguments for cusotm helper + * 160743e1c8 better error message when unmatch {{/foo}} tag detected + * d9a9416907 support {{&foo}} + * 8797485cfa fix {{^foo}} logic when foo is empty list + * 523b1373c4 fix handlebars custom helper interface + * a744a2d522 fix bad syntax when FLAG_RENDER_DEBUG + helpers + * 0044f7bd10 change FLAG_THIS behavoir + * b5b0739b68 support recursive context lookup now ( mustache spec , require FLAG_MUSTACHELOOKUP ) + * 096c241fce support standalone tag detection now ( mustache spec , require FLAG_MUSTACHESP ) + * cea46c9a67 support {{=<% %>=}} to set delimiter + * 131696af11 support subexpression {{helper (helper2 foo) bar}} + * 5184d41be6 support runtime/recursive partial ( require FLAG_RUNTIMEPARTIAL ) + * 6408917f76 support partial indent ( mustache spec , require FLAG_MUSTACHEPAIN ) + +v0.13 https://github.com/zordius/lightncandy/tree/v0.13 + * align with handlebars.js 2.0.0-alpha.4 + * e5a8fe3833 fix issue #46 ( error with {{this.foo.bar}} ) + * ea131512f9 fix issue #44 ( error with some helper inline function PHP code syntax ) + * 522591a0c6 fix issue #49 ( error with some helper user function PHP code syntax ) + * c4f7e1eaac support {{foo.bar}} lookup on instance foo then property/method bar ( flagd FLAG_PROPERTY or FLAG_METHOD required ) + * 0f4c0daa4b stop simulate Javascript output for array when pass input to custom helpers + * 22d07e5f0f BIG CHANGE of custom helper interface + +v0.12 https://github.com/zordius/lightncandy/tree/v0.12 + * align with handlebars.js 2.0.0-alpha.2 + * 64db34cf65 support {{@first}} and {{@last}} + * bfa1fbef97 add new flag FLAG_SPVARS + * 10a4623dc1 remove json schema support + * 240d9fa290 only export used LCRun2 functions when compile() with FLAG_STANDALONE now + * 3fa897c98c rename LCRun2 to LCRun3 for interface changed, old none standalone templates will error with newer version. + * e0838c7418 now can output debug template map with ANSI color + * 80dbeab63d fix php warning when compile with custom helper or block custom helper + * 8ce6268b64 support Handlebars.js style custom helper + +v0.11 https://github.com/zordius/lightncandy/tree/v0.11 + * align with handlebars.js 2.0.0-alpha.2 + * a275d52c97 use php array, remove val(). + * 8834914c2a only export used custom helper into render function now + * eb6d82d871 refine option flag consts + * fc437295ed refine comments for phpdoc + * fbf116c3e2 fix for tailing ; after helper functions + * f47a2d5014 fix for wrong param when new Exception + * 94e71ebcbd add isset() check for input value + * a826b8a1ab support {{else}} in {{#each}} now + * 25dac11bb7 support {{!-- comments --}} now (this extension allow }} apperas in the comments) + * e142b6e116 support {{@root}} or {{@root.foo.bar}} now + * 58c8d84aa2 custom helper can return extra flag to change html encoded behavior now + +v0.10 https://github.com/zordius/lightncandy/tree/v0.10 + * align with handlebars.js 2.0.0-alpha.1 + * 4c9f681080 file name changed: lightncandy.inc => lightncandy.php + * e3de01081c some minor fix for json schema + * 1feec458c7 new variable handling logic, save variable name parsing time when render() . rendering performance improved 10~30%! + * 3fa897c98c rename LCRun to LCRun2 for interface changed, old none standalone templates will error with newer version. + * 43a6d33717 fix for {{../}} php warning message + * 9189ebc1e4 now auto push documents from Travis CI + * e077d0b631 support named arguments for custom helpers {{helper name=value}} + * 2331b6fe55 support block custom helpers + * 4fedaa25f7 support number value as named arguments + * 6a91ab93d2 fix for default options and php warnings + * fc157fde62 fix for doblue quoted arguments (issue #15) + +v0.9 https://github.com/zordius/lightncandy/tree/v0.9 + * align with handlebars.js 1.3 + * a55f2dd067 support both {{@index}} and {{@key}} when {{#each an_object}} + * e59f931ea7 add FLAG_JSQUOTE support + * 92b3cf58af report more than 1 error when compile() + * 93cc121bcf test for wrong variable name format in test/error.php + * 41c1b431b4 support advanced variable naming {{foo.[bar].10}} now + * 15ce1a00a8 add FLAG_EXTHELPER option + * f51337bde2 support space control {{~sometag}} or {{sometag~}} + * fe3d67802e add FLAG_SPACECTL option + * 920fbb3039 support custom helper + * 07ae71a1bf migrate into Travis CI + * ddd3335ff6 support "some string" argument + * 20f6c888d7 html encode after custom helper executed + * 10a2f45fdc add test generator + * ccd1d3ddc2 migrate to Scrutinizer , change file name LightnCandy.inc to LightnCandy.php + * 5ac8ad8d04 now is a Composer package + +v0.8 https://github.com/zordius/lightncandy/tree/v0.8 + * align with handlebars.js 1.0.12 + * aaec049 fix partial in partial not works bug + * 52706cc fix for {{#var}} and {{^var}} , now when var === 0 means true + * 4f7f816 support {{@key}} and {{@index}} in {{#each var}} + * 63aef2a prevent array_diff_key() PHP warning when {{#each}} on none array value + * 10f3d73 add more is_array() check when {{#each}} and {{#var}} + * 367247b fix {{#if}} and {{#unless}} when value is an empty array + * c76c0bb returns null if var is not exist in a template [contributed by dtelyukh@github.com] + * d18bb6d add FLAG_ECHO support + * aec2b2b fix {{#if}} and {{#unless}} when value is an empty string + * 8924604 fix variable output when false in an array + * e82c324 fix for ifv and ifvar logic difference + * 1e38e47 better logic on var name checking. now support {{0}} in the loop, but it is not handlebars.js standard + +v0.7 https://github.com/zordius/lightncandy/tree/v0.7 + * add HISTORY.md + * 777304c change compile format to include in val, isec, ifvar + * 55de127 support {{../}} in {{#each}} + * 57e90af fix parent levels detection bug + * 96bb4d7 fix bugs for {{#.}} and {{#this}} + * f4217d1 add ifv and unl 2 new methods for LCRun + * 3f1014c fix {{#this}} and {{#.}} bug when used with {{../var}} + * cbf0b28 fix {{#if}} logic error when using {{../}} + * 2b20ef8 fix {{#with}} + {{../}} bug + * 540cd44 now support FLAG_STANDALONE + * 67ac5ff support {{>partial}} + * 98c7bb1 detect unclosed token now + +v0.6 https://github.com/zordius/lightncandy/tree/v0.6 + * align with handlebarsjs 1.0.11 + * 45ac3b6 fix #with bug when var is false + * 1a46c2c minor #with logic fix. update document + * fdc753b fix #each and section logic for 018-hb-withwith-006 + * e6cc95a add FLAG_PARENT, detect template error when scan() + * 1980691 make new LCRun::val() method to normal path.val logic + * 110d24f {{#if path.var}} bug fixed + * d6ae2e6 fix {{#with path.val}} when input value is null + * 71cf074 fix for 020-hb-doteach testcase + +v0.5 https://github.com/zordius/lightncandy/tree/v0.5 + * 955aadf fix #each bug when input is a hash + * final version for following handlebarsjs 1.0.7 diff --git a/vendor/zordius/lightncandy/LICENSE.txt b/vendor/zordius/lightncandy/LICENSE.txt new file mode 100644 index 00000000..3df560d4 --- /dev/null +++ b/vendor/zordius/lightncandy/LICENSE.txt @@ -0,0 +1,19 @@ +Copyrights for code authored by Yahoo! Inc. is licensed under the following +terms: +MIT License +Copyright (c) 2013, 2014 Yahoo! Inc. All Rights Reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/zordius/lightncandy/README.md b/vendor/zordius/lightncandy/README.md new file mode 100644 index 00000000..2afd6b9c --- /dev/null +++ b/vendor/zordius/lightncandy/README.md @@ -0,0 +1,670 @@ +LightnCandy +=========== + +An extremely fast PHP implementation of handlebars ( http://handlebarsjs.com/ ) and mustache ( http://mustache.github.io/ ). + +Travis CI status: [![Unit testing](https://travis-ci.org/zordius/lightncandy.svg?branch=master)](https://travis-ci.org/zordius/lightncandy) [![Regression testing](https://travis-ci.org/zordius/HandlebarsTest.svg?branch=master)](https://travis-ci.org/zordius/HandlebarsTest) + +Scrutinizer CI status: [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/zordius/lightncandy.svg)](https://scrutinizer-ci.com/g/zordius/lightncandy/) + +Package on packagist: [![Latest Stable Version](https://poser.pugx.org/zordius/lightncandy/v/stable.svg)](https://packagist.org/packages/zordius/lightncandy) [![License](https://poser.pugx.org/zordius/lightncandy/license.svg)](https://github.com/zordius/lightncandy/blob/master/LICENSE.txt) [![Total Downloads](https://poser.pugx.org/zordius/lightncandy/downloads.svg)](https://packagist.org/packages/zordius/lightncandy) [![HHVM Status](http://hhvm.h4cc.de/badge/zordius/lightncandy.svg)](http://hhvm.h4cc.de/package/zordius/lightncandy) + +Features +-------- + +* Logicless template: mustache ( http://mustache.github.com/ ) or handlebars ( http://handlebarsjs.com/ ) . +* Compile template to **pure PHP** code. Examples: + * <a href="https://github.com/zordius/HandlebarsTest/blob/master/fixture/001-simple-vars.tmpl">Template A</a> generated <a href="https://github.com/zordius/HandlebarsTest/blob/master/fixture/001-simple-vars.php">PHP A</a> + * <a href="https://github.com/zordius/HandlebarsTest/blob/master/fixture/016-hb-eachthis.tmpl">Template B</a> generated <a href="https://github.com/zordius/HandlebarsTest/blob/master/fixture/016-hb-eachthis.php">PHP B</a> +* **FAST!** + * Runs 4~6 times faster than <a href="https://github.com/bobthecow/mustache.php">mustache.php</a>. + * Runs 4~10 times faster than <a href="https://github.com/dingram/mustache-php">mustache-php</a>. + * Runs 10~30 times faster than <a href="https://github.com/XaminProject/handlebars.php">handlebars.php</a>. + * Detail performance test reports can be found <a href="https://github.com/zordius/HandlebarsTest">here</a>, go http://zordius.github.io/HandlebarsTest/ to see charts. +* **SMALL!** single PHP file, only 110K! +* **ROBUST!** + * 100% support <a href="https://github.com/mustache/spec">mustache spec v1.1.2</a> (without lambda module) + * Supports almost all <a href="https://github.com/kasperisager/handlebars-spec">handlebars.js spec</a> + * Output <a href="https://github.com/zordius/HandlebarsTest/blob/master/FEATURES.md">SAME</a> with <a href="https://github.com/wycats/handlebars.js">handlebars.js</a> +* **FLEXIBLE!** + * Lot of <a href="#compile-options">options</a> to change features and behaviors. +* Context generation + * Analyze used features from your template (use `LightnCandy::getContext()` to get it) . +* Debug + * <a href="#template-debugging">Generate debug version template</a> + * Find out missing data when rendering template. + * Generate visually debug template. +* Standalone Template + * The compiled PHP code can run without any PHP library. You do not need to include LightnCandy when execute rendering function. + +Installation +------------ + +Use Composer ( https://getcomposer.org/ ) to install LightnCandy: + +``` +composer require zordius/lightncandy:dev-master +``` + +Or, download LightnCandy from github: + +``` +curl -LO https://github.com/zordius/lightncandy/raw/master/src/lightncandy.php +``` + +LightnCandy requirement: PHP 5.3.0+ . + +**UPGRADE NOTICE** + +* Please check <a href="HISTORY.md">HISTORY.md</a> for versions history. +* Please check <a href="UPGRADE.md">UPGRADE.md</a> for upgrade notice. + +Usage +----- +```php +// THREE STEPS TO USE LIGHTNCANDY +// Step 1. require the lib, compile template, and get the PHP code as string +require('src/lightncandy.php'); + +$template = "Welcome {{name}} , You win \${{value}} dollars!!\n"; +$phpStr = LightnCandy::compile($template); // compiled PHP code in $phpStr + +// Step 2A. (Usage 1) use LightnCandy::prepare to get rendering function +// DEPRECATED , it may require PHP setting allow_url_fopen=1 , +// and allow_url_fopen=1 is not secure . +// When allow_url_fopen = 0, prepare() will create tmp file then include it, +// you will need to add your tmp directory into open_basedir. +// YOU MAY NEED TO CHANGE PHP SETTING BY THIS WAY +$renderer = LightnCandy::prepare($phpStr); + + +// Step 2B. (Usage 2) Store your render function in a file +// You decide your compiled template file path and name, save it. +// You can load your render function by include() later. +// RECOMMENDED WAY +file_put_contents($php_inc, $phpStr); +$renderer = include($php_inc); + + +// Step 3. run native PHP render function any time +echo "Template is:\n$template\n\n"; +echo $renderer(Array('name' => 'John', 'value' => 10000)); +echo $renderer(Array('name' => 'Peter', 'value' => 1000)); +``` + +The output will be: + +``` +Template is: +Welcome {{name}} , You win ${{value}} dollars!! + + +Welcome John , You win $10000 dollars!! +Welcome Peter , You win $1000 dollars!! +``` + +Compile Options +--------------- + +You can apply more options by running `LightnCandy::compile($template, $options)`: + +```php +LightnCandy::compile($template, Array( + 'flags' => LightnCandy::FLAG_ERROR_LOG | LightnCandy::FLAG_STANDALONE +)); +``` + +Default is to compile the template as PHP, which can be run as fast as possible (flags = `FLAG_BESTPERFORMANCE`). + +* `FLAG_ERROR_LOG` : error_log() when found any template error +* `FLAG_ERROR_EXCEPTION` : throw exception when found any template error +* `FLAG_ERROR_SKIPPARTIAL` : skip 'partial not found' error/exception. Use this to align with mustache specification. +* `FLAG_NOESCAPE` : do not do any HTML escape on {{var}}. +* `FLAG_STANDALONE` : generate stand-alone PHP codes, which can be execute without including LightnCandy.php. The compiled PHP code will contain scoped user function, somehow larger. And, the performance of the template will slow 1 ~ 10%. +* `FLAG_JSTRUE` : generate 'true' or 'false' when value is true or false (JavaScript behavior). Otherwise, true/false will generate ''. +* `FLAG_JSOBJECT` : generate '[object Object]' for associated array, generate ',' separated values for array (JavaScript behavior). Otherwise, all PHP array will generate '' or 'Array'. +* `FLAG_THIS` : resolve `{{this}}` as `{{.}}` in template. Otherwise, `{{this}}` will be resolved as normal variable. +* `FLAG_WITH` : support `{{#with var}}` in template. Otherwise, `{{#with var}}` will cause template error. +* `FLAG_PARENT` : support `{{../var}}` in template. Otherwise, `{{../var}}` will cause template error. +* `FLAG_JSQUOTE` : escape `'` to `'` , <code>`</code> to ``` . Otherwise, `'` will be escaped to `'` , <code>`</code> will not be touched. +* `FLAG_ADVARNAME` : support `{{foo.[0].[#te#st].bar}}` style advanced variable naming in template. Use this flag if you wanna use `"some string"` or `(subexpresssion)` as argument. +* `FLAG_NAMEDARG` : support named arguments for custom helper `{{helper name1=val1 nam2=val2 ...}}`. +* `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` : compile partial as runtime function, This enables recursive partials or context change for partials. +* `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_PROPERTY` : support object instance attribute access. +* `FLAG_METHOD` : support object instance method access. +* `FLAG_INSTANCE` : same with `FLAG_PROPERTY` + `FLAG_METHOD` +* `FLAG_SPACECTL` : support space control `{{~ }}` or `{{ ~}}` in template. Otherwise, `{{~ }}` or `{{ ~}}` will cause template error. +* `FLAG_SPVARS` : support special variables include @root, @index, @key, @first, @last. Otherwise, compile these variable names with default parsing logic. +* `FLAG_JS` : simulate all JavaScript string conversion behavior, same with `FLAG_JSTRUE` + `FLAG_JSOBJECT`. +* `FLAG_HANDLEBARS` : support all handlebars extensions (which mustache do not supports) , same with `FLAG_THIS` + `FLAG_WITH` + `FLAG_PARENT` + `FLAG_JSQUOTE` + `FLAG_ADVARNAME` + `FLAG_NAMEDARG` + `FLAG_SLASH` + `FLAG_ELSE` + `FLAG_MUSTACHESP` + `FLAG_MUSTACHEPAIN`. +* `FLAG_HANDLEBARSJS` : align with handlebars.js behaviors, same with `FLAG_JS` + `FLAG_HANDLEBARS`. +* `FLAG_MUSTACHESP` : align line change and spacing behaviors with mustache specification. +* `FLAG_MUSTACHELOOKUP` : align recursive lookup up behaviors with mustache specification. +* `FLAG_MUSTACHEPAIN` : align partial indent behavior with mustache specification. +* `FLAG_MUSTACHESEC` : align section `{{#foo}}` context behavior with mustache specification. +* `FLAG_MUSTACHE` : support all mustache specification, same with `FLAG_ERROR_SKIPPARTIAL` + `FLAG_MUSTACHESP` + `FLAG_MUSTACHELOOKUP` + `FLAG_MUSTACHEPAIN` + `FLAG_MUSTACHESEC`. +* `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 1% ~ 7% when the data is big and looping in the template. +* `FLAG_BESTPERFORMANCE` : same with `FLAG_ECHO` 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 +--------------- + +LightnCandy supports partial when compile time. You can provide partials by `partials` option when `compile()`: + +```php +LightnCandy::compile($template, Array( + 'partials' => Array( + 'name' => 'template: {{name}}', + ), +)); +``` + +You can also provide partials by files. When `compile()`, LightnCandy will search template files from `basedir` in the option if you provided one or more. Default template file name is `*.tmpl`, you can change or add more template file extensions with `fileext` option. + +```php +// Loading partial from file system only when valid directory is provided by basedir option +// '.' means getpwd() +LightnCandy::compile($template, Array( + 'basedir' => '.' +)); + +// Multiple basedir and fileext are supported +LightnCandy::compile($template, Array( + 'flags' => LightnCandy::FLAG_STANDALONE, + 'basedir' => Array( + '/usr/local/share/handlebars/templates', + '/usr/local/share/my_project/templates', + '/usr/local/share/my_project/partials', + ), + 'fileext' => Array( + '.tmpl', + '.mustache', + '.handlebars', + ) +)); +``` + +With this setting, when you include a partial by `{{> partial_name}}`, LightnCandy will search in this order: +* /usr/local/share/handlebars/templates/partial_name.tmpl +* /usr/local/share/handlebars/templates/partial_name.mustache +* /usr/local/share/handlebars/templates/partial_name.handlebars +* /usr/local/share/my_project/templates/partial_name.tmpl +* /usr/local/share/my_project/templates/partial_name.mustache +* /usr/local/share/my_project/templates/partial_name.handlebars +* /usr/local/share/my_project/partials/partial_name.tmpl +* /usr/local/share/my_project/partials/partial_name.mustache +* /usr/local/share/my_project/partials/partial_name.handlebars + +By default, partial uses the same context with original template. If you want to change context for the partial, you may add one more argument after the partial name: + +``` +{{>partial_name .}} // Same as {{>partial_name}} +{{>partial_name foo}} // Change input context to foo, FLAG_RUNTIMEPARTIAL required +{{>partial_name ..}} // use {{..}} as new input context, FLAG_RUNTIMEPARTIAL required +``` + +Custom Helper +------------- + +Custom helper can help you deal with common template tasks, for example: provide URL and text then generate a link. To know more about custom helper, you can read original handlebars.js document here: http://handlebarsjs.com/expressions.html . + +**NOTICE**: custom helpers to handle single tag `{{xxx}}` or a section `{{#yyy}} ... {{/yyy}}` are absolutely different in LightnCandy. To know more about creating custom helpers to handle `{{#yyy}} ... {{/yyy}}`, please refer to <a href="#block-custom-helper">Block Custom Helper</a>. + +When `compile()`, LightnCandy will lookup helpers from generated custom helper name table. You can register custom helpers with `helpers` option (**NOTICE**: `FLAG_NAMEDARG` is required for named arguments, `FLAG_ADVARNAME` is required for string or subexpression arguments): + +```php +LightnCandy::compile($template, Array( + // FLAG_NAMEDARG is required if you want to use named arguments + 'flags' => LightnCandy::FLAG_HANDLEBARS + 'helpers' => Array( + // 1. You may pass your function name + // When the function is not exist, you get compile time error + // In this case, the helper name is same with function name + // Template: {{my_helper_functoin ....}} + 'my_helper_function', + + // 2. You may also provide a static call from a class + // In this case, the helper name is same with provided full name + // **DEPRECATED** It is not valid in handlebars.js + // Template: {{myClass::myStaticMethod ....}} + 'myClass::myStaticMethod', + + // 3. You may also provide an alias name for helper function + // This help you to mapping different function to a preferred helper name + // Template: {{helper_name ....}} + 'helper_name' => 'my_other_helper', + + // 4. Alias also works well for static call of a class + // This help you to mapping different function to a preferred helper name + // Template: {{helper_name2 ....}} + 'helper_name2' => 'myClass::func', + + // 5. Anonymous function should be provided with alias + // The function will be included in generaed code always + // Template: {{helper_name3 ....}} + 'helper_name3' => function ($arg1, $arg2) { + return "<a href=\"{$arg1}\">{$arg2}</a>"; + } + ) +)); +``` + +Custom Helper Interface +----------------------- + +The input arguments are processed by LightnCandy automatically, you do not need to worry about variable name processing or current context. You can also use double quoted string as input: + +``` +{{{helper name}}} // This send processed {{{name}}} into the helper +{{{helper ../name}}} // This send processed {{{../name}}} into the helper +{{{helper "Test"}}} // This send the string "Test" into the helper (FLAG_ADVARNAME is required) +{{helper "Test"}} // This send the string "Test" into the helper and escape the helper result +{{{helper "Test" ../name}}} // This send string "Test" as first parameter, + // and processed {{{../name}}} as second parameter into the helper +``` + +Your custom helper function will be executed with two arguments. The first one is noname arguments, the second one is named arguments: + +```php +function myhelper ($args, $named) { + if (count($args)) { + // handle no name arguments.... + } + // access foo=bar from $named['foo'] ... +} +``` + +In your template: + +``` +{{{helper name=value}}} // This send processed {{{value}}} into $named['name'] +{{{helper name="value"}}} // This send the string "value" into $named['name'] +{{{helper [na me]="value"}}} // You can still protect the name with [ ] + // so you get $named['na me'] as the string 'value' +{{{helper url name="value"}}} // This send processed {{{url}}} into $args[0] + // and the string "value" into $named['name'] +``` + +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. + +When you need to do different escaping logic, you can return extended information by Array($responseString, $escape_flag) , here are some custom helper return value cases: + +```php +// 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!'; + +// Same as above because the escape_flag is DEFAULT +// 0, false, null, undefined, or '' means DEFAULT +return Array('The U&ME Helper is executed!'); +return Array('The U&ME Helper is executed!', false); +return Array('The U&ME Helper is executed!', 0); + +// escaping is handled by the helper, lightncandy will do nothing +// No matter in {{ }} or {{{ }}} , you get 'Exact&Same output \' \" Ya!' +return Array('Exact&Same output \' " Ya!', 'raw'); + +// force lightncandy escaping the helper result +// No matter in {{ }} or {{{ }}} , you get 'Not&Same output ' " Ya!' +return Array('Not&Same output \' " Ya!', 'enc'); + +// force lightncandy escaping the helper result in handlebars.js way +// No matter in {{ }} or {{{ }}} , you get 'Not&Same output ' " Ya!' +return Array('Not&Same output \' " Ya!', 'encq'); +``` + +Block Custom Helper +------------------- + +Block custom helper must be used as a section, the section is started with `{{#helper_name ...}}` and ended with `{{/helper_name}}`. + +You may use block custom helper to: + +1. Provide advanced condition logic which is different from `{{#if ...}}` ... `{{/if}}` . +2. Modify current context for the inner block. +3. Provide different context to the inner block. + +You can register block custom helpers with `blockhelpers` option: + +```php +LightnCandy::compile($template, Array( + 'blockhelpers' => Array( // The usage of blockhelpers option is similar with helpers option. + 'my_helper_function', // You can use function name, class name with static method, + ... // and choose preferred helper name by providing key name. + ) +)); +``` + +Block Custom Helper Interface +----------------------------- + +LightnCandy handled all input arguments for you, you will receive current context and parsed arguments. The return value of helper function will become new context then be passed into inner block. If you do not return any value, or return null, the inner block will not be rendered. For example: + +```php +// Only render inner block when input > 5 +// {{#helper_iffivemore total_people}}More then 5 people, discount!{{/helper_iffivemore}} +function helper_iffivemore($cx, $args, $named) { + return $args[0] > 5 ? $cx : null; +} + +// You can use named arguments, too +// {{#helper_if value=people logic="more" tovalue=5}}Yes the logic is true{{/helper_if}} +function helper_if($cx, $args, $named) { + switch ($args['logic']) { + case 'more': + return $named['value'] > $named['tovalue'] ? $cx : null; + case 'less': + return $named['value'] < $named['tovalue'] ? $cx : null; + case 'eq': + return $named['value'] == $named['tovalue'] ? $cx : null; + } +} + +// Provide default values for name and salary +// {{#helper_defaultpeople}}Hello, {{name}} ....Your salary will be {{salary}}{{/helper_defaultpeople}} +function helper_defaultpeople($cx, $args, $named) { + if (!isset($cx['name'])) { + $cx['name'] = 'Sir'; + } + $cx['salary'] = isset($cx['salary']) ? '$' . $cx['salary'] : 'unknown'; + return $cx; +} + +// Provide specific context to innerblock +// {{#helper_sample}}Preview Name:{{name}} , Salary:{{salary}}.{{/helper_sample}} +function helper_sample($cx, $args) { + return Array('name' => 'Sample Name', 'salary' => 'Sample Salary'); +} +``` + +You cannot provide new rendered result, nor handle loop in your block custom helper. To provide different rendering result, you should use <a href="#custom-helper">custom helper</a>. To handle loop, you should use `{{#each}}` . For example: + +```php +// Provide specific context to innerblock, then loop on it. +// {{#helper_categories}}{{#each .}}<li><a href="?id={{id}}">{{name}}</a></li>{{/each}}{{/helper_categories}} +function helper_categories($cx, $args) { + return getMyCategories(); // Array('category1', 'category2', ...) +} +``` + +The mission of a block custom helper is only focus on providing different context or logic to inner block, nothing else. If you like to do things beyond these restrictions, please using `hbhelpers` and keep reading to next section. + +Handlebars.js' Custom Helper +---------------------------- + +You can implement helpers more like Handlebars.js way with `hbhelpers` option, all matched single custom helper and block custom helper will be handled. In Handlebars.js, a block custom helper can rendener child block by executing options->fn, and change context by send new context as first parameter. Here are some examples to explain the behavior of `hbhelpers` custom helper: + +**#mywith (context change)** +* LightnCandy +```php +// LightnCandy sample, #mywith works same with #with +$php = LightnCandy::compile($template, Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS, + 'hbhelpers' => Array( + 'mywith' => function ($context, $options) { + return $options['fn']($context); + } + ) +)); +``` + +* Handlebars.js +```javascript +// Handlebars.js sample, #mywith works same with #with +Handlebars.registerHelper('mywith', function(context, options) { + return options.fn(context); +}); +``` + +**#myeach (context change)** +* LightnCandy +```php +// LightnCandy sample, #myeach works same with #each +$php = LightnCandy::compile($template, Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS, + 'hbhelpers' => Array( + 'myeach' => function ($context, $options) { + $ret = ''; + foreach ($context as $cx) { + $ret .= $options['fn']($cx); + } + return $ret; + } + ) +)); +``` + +* Handlebars.js +```javascript +// 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 +```php +// LightnCandy sample, #myif works same with #if +$php = LightnCandy::compile($template, Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS, + 'hbhelpers' => Array( + 'myif' => function ($conditional, $options) { + if ($conditional) { + return $options['fn'](); + } else { + return $options['inverse'](); + } + } + ) +)); +``` + +* Handlebars.js +```javascript +// 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); + } +}); +``` + +**Hashed arguments** +* LightnCandy +```php +$php = LightnCandy::compile($template, Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS, + 'hbhelpers' => Array( + 'sample' => function ($arg1, $arg2, $options) { + // All hashed arguments are in $options['hash'] + } + ) +)); +``` + +* Handlebars.js +```javascript +Handlebars.registerHelper('sample', function(arg1, arg2, options) { + // All hashed arguments are in options.hash +}); +``` + +**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 <a href="#custom-helper-escaping">Custom Helper Escaping</a> for more. + +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 LCRun3 options into render function to know more rendering error (missing data). For example: + +```php +$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: +// LCRun3: [gender] is not exist +// LCRun3: ../[test] is not exist +$renderer(Array('name' => 'John'), LCRun3::DEBUG_ERROR_LOG); + +// Output visual debug template with ANSI color: +echo $renderer(Array('name' => 'John'), LCRun3::DEBUG_TAGS_ANSI); + +// Output debug template with HTML comments: +echo $renderer(Array('name' => 'John'), LCRun3::DEBUG_TAGS_HTML); +``` + +The ANSI output will be: + +<a href="tests/example_debug.php"><img src="example_debug.png"/></a> + +Here are the list of LCRun3 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 + +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 +$php = LightnCandy::compile($template, Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS, + 'renderex' => '// Compiled at ' . date('Y-m-d h:i:s') +)); +``` + +Your render function will be: + +```php +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. + +Unsupported Feature (so far) +---------------------------- + +* [NEVER] `{{foo/bar}}` style variable name, it is deprecated in official handlebars.js document. +* [maybe] mustache lambda : runtime time compile based on input value is far from lightncandy nature, not in the plan now. + +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: `{{{"{{"}}}`, prevent using `\{{`. +* 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 (require `FLAG_MUSTACHESP`) +* 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 (require `FLAG_MUSTACHEPAIN`) +* 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}}` : 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`) +* `{{#unless var}}` : run unless logic with original scope (null, false, empty Array and '' will render this block) +* `{{#with var}}` : change context scope. If the var is false, skip included section. (require `FLAG_WITH`) +* `{{../var}}` : parent template scope. (require `FLAG_PARENT`) +* `{{>file}}` : partial; include another template inside a template. +* `{{>file foo}}` : partial with new context (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 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`) diff --git a/vendor/zordius/lightncandy/UPGRADE.md b/vendor/zordius/lightncandy/UPGRADE.md new file mode 100644 index 00000000..01f4c15b --- /dev/null +++ b/vendor/zordius/lightncandy/UPGRADE.md @@ -0,0 +1,18 @@ +Upgrade Notice +============== + +* Standalone templates compiled by older LightnCandy can be executed safe when you upgrade to any new version of LightnCandy. + +* Recompile your none standalone templates when you upgrade LightnCandy. + +Version v0.13 +------------- +* The interface of custom helpers was changed from v0.13 . if you use this feature you may need to modify your custom helper functions. + +Version v0.11 +------------- +* Due to big change of render() debugging, the rendering support class `LCRun2` is renamed to `LCRun3`. If you compile templates as none standalone PHP code by LightnCandy v0.11 or before, you should compile these templates again. Or, you may run into `Class 'LCRun2' not found` error when you execute these old rendering functions. + +Version v0.9 +------------ +* Due to big change of variable name handling, the rendering support class `LCRun` is renamed to `LCRun2`. If you compile templates as none standalone PHP code by LightnCandy v0.9 or before, you should compile these templates again. Or, you may run into `Class 'LCRun' not found` error when you execute these old rendering functions. diff --git a/vendor/zordius/lightncandy/build/gen_doc b/vendor/zordius/lightncandy/build/gen_doc new file mode 100644 index 00000000..428e619d --- /dev/null +++ b/vendor/zordius/lightncandy/build/gen_doc @@ -0,0 +1,6 @@ +#!/bin/sh +curl -O https://cloud.github.com/downloads/apigen/apigen/ApiGen-2.8.0-standalone.zip +unzip -oq ApiGen-2.8.0-standalone.zip +rm ApiGen-2.8.0-standalone.zip +php -dopen_basedir=/ apigen/apigen.php --source src/ --destination build/result/docs/ --template-config apigen/templates/bootstrap/config.neon --deprecated yes +rm -rf apigen diff --git a/vendor/zordius/lightncandy/build/gen_test.php b/vendor/zordius/lightncandy/build/gen_test.php new file mode 100644 index 00000000..97d49489 --- /dev/null +++ b/vendor/zordius/lightncandy/build/gen_test.php @@ -0,0 +1,64 @@ +<?php + +foreach (Array( + 'vendor/phpunit/phpunit/PHPUnitPHPUnit/Autoload.php', + 'PHPUnit/Autoload.php', + 'src/lightncandy.php' +) as $inc) { + if (file_exists($inc)) { + include_once($inc); + break; + } +} + +genTestForClass('LightnCandy'); +genTestForClass('LCRun3'); + +function genTestForClass($classname) { + ob_start(); + + echo <<<VAR +<?php +/** + * Generated by build/gen_test + */ +require_once('src/lightncandy.php'); + +class {$classname}Test extends PHPUnit_Framework_TestCase +{ + +VAR + ; + + $class = new ReflectionClass($classname); + foreach ($class->getMethods() as $method) { + if (preg_match_all('/@expect (.+) when input (.+)( after (.+))?/', $method->getDocComment(), $matched)) { + echo <<<VAR + /** + * @covers {$classname}::{$method->name} + */ + public function testOn_{$method->name}() { + \$method = new ReflectionMethod('$classname', '{$method->name}'); + +VAR + ; + if ($method->isPrivate() || $method->isProtected()) { + echo " \$method->setAccessible(true);\n"; + } + foreach ($matched[1] as $idx => $expect) { + if ($matched[3][$idx]) { + echo " {$matched[3][$idx]}\n"; + } + echo " \$this->assertEquals($expect, \$method->invoke(null,\n {$matched[2][$idx]}\n ));\n"; + } + echo " }\n"; + } + } + echo "}\n?>"; + + $fn = "tests/{$classname}Test.php"; + if (!file_put_contents($fn, ob_get_clean())) { + die("Can not generate tests into file $fn !!\n"); + } +} +?> diff --git a/vendor/zordius/lightncandy/build/push_ghpage b/vendor/zordius/lightncandy/build/push_ghpage new file mode 100644 index 00000000..de64e2b9 --- /dev/null +++ b/vendor/zordius/lightncandy/build/push_ghpage @@ -0,0 +1,12 @@ +#!/bin/sh +git checkout -B gh-pages +git pull origin gh-pages +rm * +rm -rf src +rm -rf test +cp -r build/result/docs/* . +rm -rf build +rm .travis.yml +git add . +git commit -a -m "New documents on github" +git push origin gh-pages diff --git a/vendor/zordius/lightncandy/build/runphp b/vendor/zordius/lightncandy/build/runphp new file mode 100644 index 00000000..aa694de3 --- /dev/null +++ b/vendor/zordius/lightncandy/build/runphp @@ -0,0 +1,2 @@ +#!/bin/sh +php -dopen_basedir=/ $1 $2 diff --git a/vendor/zordius/lightncandy/build/travis_push b/vendor/zordius/lightncandy/build/travis_push new file mode 100644 index 00000000..b51e4dcc --- /dev/null +++ b/vendor/zordius/lightncandy/build/travis_push @@ -0,0 +1,60 @@ +#!/bin/sh +echo "DEBUG ENV: ${TRAVIS_JOB_NUMBER} , ${TRAVIS_BUILD_NUMBER} , ${TRAVIS_PULL_REQUEST} ..." + +if [ "${TRAVIS_PULL_REQUEST}" != "false" ]; then + echo "This is a PR, skip push." + exit 0 +fi + +if [ "${TRAVIS_BUILD_NUMBER}.1" != "${TRAVIS_JOB_NUMBER}" ]; then + echo "Only push documents 1 time... quit." + exit 0 +fi + +# Push coverage report +wget https://scrutinizer-ci.com/ocular.phar +php ocular.phar code-coverage:upload --format=php-clover coverage.clover + +# Set for all push in this script. +git config --global user.name "Travis-CI" +git config --global user.email "zordius@yahoo-inc.com" + +# Generate ANSI sample +git clone https://github.com/fcambus/ansilove +php tests/example_debug.php > example_debug +php ansilove/ansilove example_debug +git add example_debug.png + +# Push new tests back to this branch +git commit -a -m "Auto generated tests from Travis [ci skip]" +git push "https://${GHTK}@github.com/zordius/lightncandy.git" HEAD:${TRAVIS_BRANCH} > /dev/null 2>&1 + +# Update hash in HandlebarsTest and push back, trigger new tests there. +git clone https://github.com/zordius/HandlebarsTest +cd HandlebarsTest +echo ${TRAVIS_COMMIT} > lightncandy +git add lightncandy +git commit -a -m "Auto test on zordius/lightncandy@${TRAVIS_COMMIT}" +git push "https://${GHTK}@github.com/zordius/HandlebarsTest.git" > /dev/null 2>&1 +cd .. + +# Generate documents for this branch +build/gen_doc +cd build/result/docs + +if [ "${TRAVIS_BRANCH}" != "master" ]; then + echo "Document will be pushed here: http://zordius.github.io/lightncandy/${TRAVIS_BRANCH}/" + cd .. + git init + git pull --quiet "https://${GHTK}@github.com/zordius/lightncandy.git" gh-pages:master > /dev/null 2>&1 + rm -rf $TRAVIS_BRANCH + mv docs $TRAVIS_BRANCH + git add $TRAVIS_BRANCH +else + echo "Document will be pushed here: http://zordius.github.io/lightncandy/" + git init + git add . +fi + +git commit -m "Auto deployed to Github Pages from branch ${TRAVIS_BRANCH} @${TRAVIS_COMMIT} [ci skip]" +git push --force --quiet "https://${GHTK}@github.com/zordius/lightncandy.git" master:gh-pages > /dev/null 2>&1 diff --git a/vendor/zordius/lightncandy/composer.json b/vendor/zordius/lightncandy/composer.json new file mode 100644 index 00000000..bd15cc2c --- /dev/null +++ b/vendor/zordius/lightncandy/composer.json @@ -0,0 +1,22 @@ +{ + "name": "zordius/lightncandy", + "description": "An extremely fast PHP implementation of handlebars ( http://handlebarsjs.com/ ) and mustache ( http://mustache.github.io/ ).", + "homepage": "https://github.com/zordius/lightncandy", + "keywords": ["handlebars", "mustache", "PHP", "template", "logicless"], + "license": "MIT", + "authors": [ + { + "name": "Zordius Chen", + "email": "zordius@yahoo-inc.com" + } + ], + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "4.0.17" + }, + "autoload": { + "classmap": ["src/lightncandy.php"] + } +} diff --git a/vendor/zordius/lightncandy/example_debug.png b/vendor/zordius/lightncandy/example_debug.png Binary files differnew file mode 100644 index 00000000..b117ee87 --- /dev/null +++ b/vendor/zordius/lightncandy/example_debug.png diff --git a/vendor/zordius/lightncandy/phpunit.xml b/vendor/zordius/lightncandy/phpunit.xml new file mode 100644 index 00000000..4e9417f1 --- /dev/null +++ b/vendor/zordius/lightncandy/phpunit.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<phpunit + backupGlobals="false" + colors="true" + convertNoticesToExceptions="false" +> + <filter> + <whitelist> + <directory suffix=".php">./src</directory> + </whitelist> + </filter> + + <logging> + <log type="coverage-html" target="build/result/coverage" title="lightncandy" charset="UTF-8" /> + <log type="coverage-text" target="php://stdout" /> + </logging> + + <testsuites> + <testsuite name="LightnCandy Test Suite"> + <directory>./tests/</directory> + </testsuite> + </testsuites> +</phpunit> diff --git a/vendor/zordius/lightncandy/src/lightncandy.php b/vendor/zordius/lightncandy/src/lightncandy.php new file mode 100644 index 00000000..d7a62e57 --- /dev/null +++ b/vendor/zordius/lightncandy/src/lightncandy.php @@ -0,0 +1,2560 @@ +<?php +/* + +Copyrights for code authored by Yahoo! Inc. is licensed under the following terms: +MIT License +Copyright (c) 2013 Yahoo! Inc. All Rights Reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Origin: https://github.com/zordius/lightncandy +*/ + +/** + * This is abstract engine which defines must-have methods. + * + * @package LightnCandy + * @author Zordius <zordius@yahoo-inc.com> + */ + +/** + * LightnCandy static core class. + */ +class LightnCandy { + // Compile time error handling flags + const FLAG_ERROR_LOG = 1; + const FLAG_ERROR_EXCEPTION = 2; + const FLAG_ERROR_SKIPPARTIAL = 4194304; + + // Compile the template as standalone PHP code which can execute without including LightnCandy + const FLAG_STANDALONE = 4; + const FLAG_NOESCAPE = 67108864; + + // JavaScript compatibility + const FLAG_JSTRUE = 8; + const FLAG_JSOBJECT = 16; + + // Handlebars.js compatibility + const FLAG_THIS = 32; + const FLAG_WITH = 64; + const FLAG_PARENT = 128; + const FLAG_JSQUOTE = 256; + const FLAG_ADVARNAME = 512; + const FLAG_SPACECTL = 1024; + const FLAG_NAMEDARG = 2048; + const FLAG_SPVARS = 4096; + const FLAG_SLASH = 8388608; + const FLAG_ELSE = 16777216; + + // PHP behavior flags + const FLAG_EXTHELPER = 8192; + const FLAG_ECHO = 16384; + const FLAG_PROPERTY = 32768; + const FLAG_METHOD = 65536; + const FLAG_RUNTIMEPARTIAL = 1048576; + + // Mustache compatibility + const FLAG_MUSTACHESP = 131072; + const FLAG_MUSTACHELOOKUP = 262144; + const FLAG_MUSTACHEPAIN = 2097152; + const FLAG_MUSTACHESEC = 33554432; + + // Template rendering time debug flags + const FLAG_RENDER_DEBUG = 524288; + + // alias flags + const FLAG_BESTPERFORMANCE = 16384; // FLAG_ECHO + const FLAG_JS = 24; // FLAG_JSTRUE + FLAG_JSOBJECT + const FLAG_MUSTACHE = 40239104; // FLAG_ERROR_SKIPPARTIAL + FLAG_MUSTACHESP + FLAG_MUSTACHELOOKUP + FLAG_MUSTACHEPAIN + FLAG_MUSTACHESEC + const FLAG_HANDLEBARS = 27402208; // FLAG_THIS + FLAG_WITH + FLAG_PARENT + FLAG_JSQUOTE + FLAG_ADVARNAME + FLAG_SPACECTL + FLAG_NAMEDARG + FLAG_SPVARS + FLAG_SLASH + FLAG_ELSE + FLAG_MUSTACHESP + FLAG_MUSTACHEPAIN + const FLAG_HANDLEBARSJS = 27402232; // FLAG_JS + FLAG_HANDLEBARS + const FLAG_INSTANCE = 98304; // FLAG_PROPERTY + FLAG_METHOD + + // RegExps + const VARNAME_SEARCH = '/(\\[[^\\]]+\\]|[^\\[\\]\\.]+)/'; + const EXTENDED_COMMENT_SEARCH = '/{{!--.*?--}}/s'; + + // Positions of matched token + const POS_LOTHER = 1; + const POS_LSPACE = 2; + const POS_BEGINTAG = 3; + const POS_LSPACECTL = 4; + const POS_OP = 5; + const POS_INNERTAG = 6; + const POS_RSPACECTL = 7; + const POS_ENDTAG = 8; + const POS_RSPACE = 9; + const POS_ROTHER = 10; + + protected static $lastContext; + + /** + * Compile handlebars template into PHP code. + * + * @param string $template handlebars template string + * @param array<string,array|string|integer> $options LightnCandy compile time and run time options, default is array('flags' => LightnCandy::FLAG_BESTPERFORMANCE) + * + * @return string|false Compiled PHP code when successed. If error happened and compile failed, return false. + */ + public static function compile($template, $options = array('flags' => self::FLAG_BESTPERFORMANCE)) { + $context = static::buildContext($options); + + if (static::handleError($context)) { + return false; + } + + // Strip extended comments + $template = preg_replace(static::EXTENDED_COMMENT_SEARCH, '{{!*}}', $template); + + // Do first time scan to find out used feature, detect template error. + static::setupToken($context); + static::verifyTemplate($context, $template); + + if (static::handleError($context)) { + return false; + } + + // Do PHP code generation. + static::setupToken($context); + $code = static::compileTemplate($context, static::escapeTemplate($template)); + + // return false when fatal error + if (static::handleError($context)) { + return false; + } + + // Or, return full PHP render codes as string + return static::composePHPRender($context, $code); + } + + /* + * Escape template + * + * @param string $template handlebars template string + * + * @return string Escaped template + * + * @expect 'abc' when input 'abc' + * @expect 'a\\bc' when input 'a\bc' + * @expect 'a\\\'bc' when input 'a\'bc' + */ + protected static function escapeTemplate($template) { + return addcslashes(addcslashes($template, '\\'), "'"); + } + + /** + * Setup token delimiter by default or provided string + * + * @param array<string,array|string|integer> $context Current context + * @param string $left left string of a token + * @param string $right right string of a token + */ + protected static function setupToken(&$context, $left = '{{', $right = '}}') { + if (preg_match('/=/', "$left$right")) { + $context['error'][] = "Can not set delimiter contains '=' , you try to set delimiter as '$left' and '$right'."; + return; + } + + $context['tokens']['startchar'] = substr($left, 0, 1); + $context['tokens']['left'] = $left; + $context['tokens']['right'] = $right; + + if (($left === '{{') && ($right === '}}')) { + $left = '\\{{2,3}'; + $right = '\\}{2,3}'; + } else { + $left = preg_quote($left); + $right = preg_quote($right); + } + + $context['tokens']['search'] = "/^(.*?)(\\s*)($left)(~?)([\\^#\\/!&>]?)(.*?)(~?)($right)(\\s*)(.*)\$/s"; + } + + /** + * Verify template and scan for used features + * + * @param array<string,array|string|integer> $context Current context + * @param string $template handlebars template + */ + protected static function verifyTemplate(&$context, $template) { + while (preg_match($context['tokens']['search'], $template, $matches)) { + $context['tokens']['count']++; + static::scanFeatures($matches, $context); + $template = $matches[self::POS_ROTHER]; + } + } + + /** + * Compile template into PHP code (internal method) + * + * @param array<string,array|string|integer> $context Current context + * @param string $template handlebars template + * @param string $partial partial name when $template is come from the template + * + * @return string generated PHP code + */ + protected static function compileTemplate(&$context, $template, $partial = '') { + // Check for recursive partial + if ($partial && !$context['flags']['runpart']) { + $context['partialStack'][] = $partial; + $diff = count($context['partialStack']) - count(array_unique($context['partialStack'])); + if ($diff > 1) { + $context['error'][] = "Skip rendering partial '$partial' again due to recursive detected"; + return ''; + } + if ($diff) { + $context['error'][] = 'I found recursive partial includes as the path: ' . implode(' -> ', $context['partialStack']) . '! You should fix your template or compile with LightnCandy::FLAG_RUNTIMEPARTIAL flag.'; + } + } + + $code = ''; + while (preg_match($context['tokens']['search'], $template, $matches)) { + // Skip a token when it is slash escaped + if ($context['flags']['slash'] && ($matches[self::POS_LSPACE] === '') && preg_match('/^(.*?)(\\\\+)$/s', $matches[self::POS_LOTHER], $escmatch)) { + if (strlen($escmatch[2]) % 4) { + $code .= substr($matches[self::POS_LOTHER], 0, -2) . $context['tokens']['startchar']; + $matches[self::POS_BEGINTAG] = substr($matches[self::POS_BEGINTAG], 1); + $template = implode('', array_slice($matches, self::POS_BEGINTAG)); + continue; + } else { + $matches[self::POS_LOTHER] = $escmatch[1] . str_repeat('\\', strlen($escmatch[2]) / 2); + } + } + + $context['tokens']['current']++; + $tmpl = static::compileToken($matches, $context); + if ($tmpl == $context['ops']['seperator']) { + $tmpl = ''; + } else { + $tmpl = "'$tmpl'"; + } + $code .= "{$matches[self::POS_LOTHER]}{$matches[self::POS_LSPACE]}$tmpl"; + $template = "{$matches[self::POS_RSPACE]}{$matches[self::POS_ROTHER]}"; + } + + if ($partial && !$context['flags']['runpart']) { + array_pop($context['partialStack']); + } + + return "$code$template"; + } + + /** + * Compose LightnCandy render codes for include() + * + * @param array<string,array|string|integer> $context Current context + * @param string $code generated PHP code + * + * @return string Composed PHP code + */ + protected static function composePHPRender($context, $code) { + $flagJStrue = static::getBoolStr($context['flags']['jstrue']); + $flagJSObj = static::getBoolStr($context['flags']['jsobj']); + $flagSPVar = static::getBoolStr($context['flags']['spvar']); + $flagProp = static::getBoolStr($context['flags']['prop']); + $flagMethod = static::getBoolStr($context['flags']['method']); + $flagMustlok = static::getBoolStr($context['flags']['mustlok']); + $flagMustsec = static::getBoolStr($context['flags']['mustsec']); + $flagEcho = static::getBoolStr($context['flags']['echo']); + + $libstr = static::exportLCRun($context); + $constants = static::exportLCRunConstant($context); + $helpers = static::exportHelper($context); + $bhelpers = static::exportHelper($context, 'blockhelpers'); + $hbhelpers = static::exportHelper($context, 'hbhelpers'); + $debug = LCRun3::DEBUG_ERROR_LOG; + + // Return generated PHP code string. + return "<?php return function (\$in, \$debugopt = $debug) { + \$cx = array( + 'flags' => array( + 'jstrue' => $flagJStrue, + 'jsobj' => $flagJSObj, + 'spvar' => $flagSPVar, + 'prop' => $flagProp, + 'method' => $flagMethod, + 'mustlok' => $flagMustlok, + 'mustsec' => $flagMustsec, + 'echo' => $flagEcho, + 'debug' => \$debugopt, + ), + 'constants' => $constants, + 'helpers' => $helpers, + 'blockhelpers' => $bhelpers, + 'hbhelpers' => $hbhelpers, + 'partials' => array({$context['partialCode']}), + 'scopes' => array(\$in), + 'sp_vars' => array('root' => \$in), +$libstr + ); + {$context['renderex']} + {$context['ops']['op_start']}'$code'{$context['ops']['op_end']} +} +?>"; + } + + /** + * Build context from options + * + * @param array<string,array|string|integer> $options input options + * + * @return array<string,array|string|integer> Context from options + */ + protected static function buildContext($options) { + if (!is_array($options)) { + $options = array(); + } + + $flags = isset($options['flags']) ? $options['flags'] : self::FLAG_BESTPERFORMANCE; + + $context = array( + 'flags' => array( + 'errorlog' => $flags & self::FLAG_ERROR_LOG, + 'exception' => $flags & self::FLAG_ERROR_EXCEPTION, + 'skippartial' => $flags & self::FLAG_ERROR_SKIPPARTIAL, + 'standalone' => $flags & self::FLAG_STANDALONE, + 'noesc' => $flags & self::FLAG_NOESCAPE, + 'jstrue' => $flags & self::FLAG_JSTRUE, + 'jsobj' => $flags & self::FLAG_JSOBJECT, + 'jsquote' => $flags & self::FLAG_JSQUOTE, + 'this' => $flags & self::FLAG_THIS, + 'with' => $flags & self::FLAG_WITH, + 'parent' => $flags & self::FLAG_PARENT, + 'echo' => $flags & self::FLAG_ECHO, + 'advar' => $flags & self::FLAG_ADVARNAME, + 'namev' => $flags & self::FLAG_NAMEDARG, + 'spvar' => $flags & self::FLAG_SPVARS, + 'slash' => $flags & self::FLAG_SLASH, + 'else' => $flags & self::FLAG_ELSE, + 'exhlp' => $flags & self::FLAG_EXTHELPER, + 'mustsp' => $flags & self::FLAG_MUSTACHESP, + 'mustlok' => $flags & self::FLAG_MUSTACHELOOKUP, + 'mustpi' => $flags & self::FLAG_MUSTACHEPAIN, + 'mustsec' => $flags & self::FLAG_MUSTACHESEC, + 'debug' => $flags & self::FLAG_RENDER_DEBUG, + 'prop' => $flags & self::FLAG_PROPERTY, + 'method' => $flags & self::FLAG_METHOD, + 'runpart' => $flags & self::FLAG_RUNTIMEPARTIAL, + ), + 'level' => 0, + 'stack' => array(), + 'error' => array(), + 'basedir' => static::buildCXBasedir($options), + 'fileext' => static::buildCXFileext($options), + 'tokens' => array( + 'standalone' => true, + 'ahead' => false, + 'current' => 0, + 'count' => 0, + 'partialind' => '', + ), + 'usedPartial' => array(), + 'partialStack' => array(), + 'partialCode' => '', + 'usedFeature' => array( + 'rootthis' => 0, + 'enc' => 0, + 'raw' => 0, + 'sec' => 0, + 'isec' => 0, + 'if' => 0, + 'else' => 0, + 'unless' => 0, + 'each' => 0, + 'this' => 0, + 'parent' => 0, + 'with' => 0, + 'dot' => 0, + 'comment' => 0, + 'partial' => 0, + 'helper' => 0, + 'bhelper' => 0, + 'hbhelper' => 0, + 'delimiter' => 0, + ), + 'usedCount' => array( + 'var' => array(), + 'helpers' => array(), + 'blockhelpers' => array(), + 'hbhelpers' => array(), + 'lcrun' => array(), + ), + 'partials' => (isset($options['partials']) && is_array($options['partials'])) ? $options['partials'] : array(), + 'helpers' => array(), + 'blockhelpers' => array(), + 'hbhelpers' => array(), + 'renderex' => isset($options['renderex']) ? $options['renderex'] : '', + ); + + $context['ops'] = $context['flags']['echo'] ? array( + 'seperator' => ',', + 'f_start' => 'echo ', + 'f_end' => ';', + 'op_start' => 'ob_start();echo ', + 'op_end' => ';return ob_get_clean();', + 'cnd_start' => ';if ', + 'cnd_then' => '{echo ', + 'cnd_else' => ';}else{echo ', + 'cnd_end' => ';}echo ', + ) : array( + 'seperator' => '.', + 'f_start' => 'return ', + 'f_end' => ';', + 'op_start' => 'return ', + 'op_end' => ';', + 'cnd_start' => '.(', + 'cnd_then' => ' ? ', + 'cnd_else' => ' : ', + 'cnd_end' => ').', + ); + + $context['ops']['enc'] = $context['flags']['jsquote'] ? 'encq' : 'enc'; + $context = static::buildHelperTable($context, $options); + $context = static::buildHelperTable($context, $options, 'blockhelpers'); + $context = static::buildHelperTable($context, $options, 'hbhelpers'); + + return $context; + } + + /** + * Build custom helper table + * + * @param array<string,array|string|integer> $context prepared context + * @param array<string,array|string|integer> $options input options + * @param string $tname helper table name + * + * @return array<string,array|string|integer> context with generated helper table + * + * @expect array() when input array(), array() + * @expect array('flags' => array('exhlp' => 1)) when input array('flags' => array('exhlp' => 1)), array('helpers' => array('abc')) + * @expect array('error' => array('Can not find custom helper function defination abc() !'), 'flags' => array('exhlp' => 0)) when input array('error' => array(), 'flags' => array('exhlp' => 0)), array('helpers' => array('abc')) + * @expect array('flags' => array('exhlp' => 1), 'helpers' => array('LCRun3::raw' => 'LCRun3::raw')) when input array('flags' => array('exhlp' => 1), 'helpers' => array()), array('helpers' => array('LCRun3::raw')) + * @expect array('flags' => array('exhlp' => 1), 'helpers' => array('test' => 'LCRun3::raw')) when input array('flags' => array('exhlp' => 1), 'helpers' => array()), array('helpers' => array('test' => 'LCRun3::raw')) + */ + protected static function buildHelperTable($context, $options, $tname = 'helpers') { + if (isset($options[$tname]) && is_array($options[$tname])) { + foreach ($options[$tname] as $name => $func) { + if (is_callable($func)) { + $context[$tname][is_int($name) ? $func : $name] = $func; + } else { + if (is_array($func)) { + $context['error'][] = "I found an array in $tname with key as $name, please fix it."; + } else { + if (!$context['flags']['exhlp']) { + $context['error'][] = "Can not find custom helper function defination $func() !"; + } + } + } + } + } + return $context; + } + + /** + * Read partial file content as string and store in context + * + * @param string $name partial name + * @param array<string,array|string|integer> $context Current context of compiler progress. + */ + protected static function readPartial($name, &$context) { + $context['usedFeature']['partial']++; + + if (isset($context['usedPartial'][$name])) { + return; + } + + $cnt = static::resolvePartial($name, $context); + + if ($cnt !== null) { + return static::compilePartial($name, $context, $cnt); + } + + if (!$context['flags']['skippartial']) { + $context['error'][] = "Can not find partial file for '$name', you should set correct basedir and fileext in options"; + } + } + + /** + * locate partial file, return the file name + * + * @param string $name partial name + * @param array<string,array|string|integer> $context Current context of compiler progress. + * + * @return string|null $content partial content + */ + protected static function resolvePartial(&$name, &$context) { + if (isset($context['partials'][$name])) { + return $context['partials'][$name]; + } + + foreach ($context['basedir'] as $dir) { + foreach ($context['fileext'] as $ext) { + $fn = "$dir/$name$ext"; + if (file_exists($fn)) { + return file_get_contents($fn); + } + } + } + return null; + } + + /** + * compile partial file, stored in context + * + * @param string $name partial name + * @param array<string,array|string|integer> $context Current context of compiler progress. + * @param string $content partial content + */ + protected static function compilePartial(&$name, &$context, $content) { + $context['usedPartial'][$name] = static::escapeTemplate($content); + + $originalAhead = $context['tokens']['ahead']; + $tmpContext = $context; + $tmpContext['level'] = 0; + static::setupToken($tmpContext); + + static::verifyTemplate($tmpContext, $content); + $originalToken = $context['tokens']; + $context = $tmpContext; + $context['tokens'] = $originalToken; + $context['tokens']['ahead'] = $originalAhead; + + if ($context['flags']['runpart']) { + $code = static::compileTemplate($context, $context['usedPartial'][$name], $name); + if ($context['flags']['mustpi']) { + $sp = ', $sp'; + $code = preg_replace('/^/m', "'{$context['ops']['seperator']}\$sp{$context['ops']['seperator']}'", $code); + } else { + $sp = ''; + } + $context['partialCode'] .= "'$name' => function (\$cx, \$in{$sp}) {{$context['ops']['op_start']}'$code'{$context['ops']['op_end']}},"; + } + } + + /** + * Internal method used by compile(). Check options and handle fileext. + * + * @param array<string,array|string|integer> $options current compile option + * + * @return array<string> file extensions + * + * @expect array('.tmpl') when input array() + * @expect array('test') when input array('fileext' => 'test') + * @expect array('test1') when input array('fileext' => array('test1')) + * @expect array('test2', 'test3') when input array('fileext' => array('test2', 'test3')) + */ + protected static function buildCXFileext($options) { + $exts = isset($options['fileext']) ? $options['fileext'] : '.tmpl'; + return is_array($exts) ? $exts : array($exts); + } + + /** + * Internal method used by compile(). Check options and handle basedir. + * + * @param array<string,array|string|integer> $options current compile option + * + * @return array<string> base directories + * + * @expect array() when input array() + * @expect array() when input array('basedir' => array()) + * @expect array('src') when input array('basedir' => array('src')) + * @expect array('src') when input array('basedir' => array('src', 'dir_not_found')) + * @expect array('src', 'tests') when input array('basedir' => array('src', 'tests')) + */ + protected static function buildCXBasedir($options) { + $dirs = isset($options['basedir']) ? $options['basedir'] : 0; + $dirs = is_array($dirs) ? $dirs : array($dirs); + $ret = array(); + + foreach ($dirs as $dir) { + if (is_string($dir) && is_dir($dir)) { + $ret[] = $dir; + } + } + + return $ret; + } + + /** + * Internal method used by compile(). Get PHP code from a closure of function as string. + * + * @param object $closure Closure object + * + * @return string + * + * @expect 'function($a) {return;}' when input function ($a) {return;} + * @expect 'function($a) {return;}' when input function ($a) {return;} + */ + protected static function getPHPCode($closure) { + if (is_string($closure) && preg_match('/(.+)::(.+)/', $closure, $matched)) { + $ref = new ReflectionMethod($matched[1], $matched[2]); + } else { + $ref = new ReflectionFunction($closure); + } + $fname = $ref->getFileName(); + + $lines = file_get_contents($fname); + $file = new SplFileObject($fname); + $file->seek($ref->getStartLine() - 2); + $spos = $file->ftell(); + $file->seek($ref->getEndLine() - 1); + $epos = $file->ftell(); + + return preg_replace('/^.*?function(\s+[^\s\\(]+?)?\s*?\\((.+?)\\}[,\\s]*;?$/s', 'function($2}', substr($lines, $spos, $epos - $spos)); + } + + /** + * Internal method used by compile(). Export required custom helper functions. + * + * @param string $tname helper table name + * @param array<string,array|string|integer> $context current compile context + * + * @return string + */ + protected static function exportHelper($context, $tname = 'helpers') { + $ret = ''; + foreach ($context[$tname] as $name => $func) { + if (!isset($context['usedCount'][$tname][$name])) { + continue; + } + if ((is_object($func) && ($func instanceof Closure)) || ($context['flags']['exhlp'] == 0)) { + $ret .= (" '$name' => " . static::getPHPCode($func) . ",\n"); + continue; + } + $ret .= " '$name' => '$func',\n"; + } + + return "array($ret)"; + } + + /** + * Internal method used by compile(). Export required standalone functions. + * + * @param array<string,array|string|integer> $context current compile context + * + * @return string + */ + protected static function exportLCRun($context) { + if ($context['flags']['standalone'] == 0) { + return ''; + } + + $class = new ReflectionClass('LCRun3'); + $fname = $class->getFileName(); + $lines = file_get_contents($fname); + $file = new SplFileObject($fname); + $methods = array(); + $ret = "'funcs' => array(\n"; + + foreach ($class->getMethods() as $method) { + $name = $method->getName(); + $file->seek($method->getStartLine() - 2); + $spos = $file->ftell(); + $file->seek($method->getEndLine() - 2); + $epos = $file->ftell(); + $methods[$name] = static::scanLCRunDependency($context, preg_replace('/public static function (.+)\\(/', '\'$1\' => function (', substr($lines, $spos, $epos - $spos))); + } + unset($file); + + $exports = array_keys($context['usedCount']['lcrun']); + + while (true) { + if (array_sum(array_map(function ($name) use (&$exports, $methods) { + $n = 0; + foreach ($methods[$name][1] as $child => $count) { + if (!in_array($child, $exports)) { + $exports[] = $child; + $n++; + } + } + return $n; + }, $exports)) == 0) { + break; + } + } + + foreach ($exports as $export) { + $ret .= ($methods[$export][0] . " },\n"); + } + + return "$ret)\n"; + } + + /** + * Internal method used by compile(). Export standalone constants. + * + * @param array<string,array|string|integer> $context current compile context + * + * @return string + */ + protected static function exportLCRunConstant($context) { + if ($context['flags']['standalone'] == 0) { + return 'array()'; + } + + $class = new ReflectionClass('LCRun3'); + $constants = $class->getConstants(); + $ret = " array(\n"; + foreach($constants as $name => $value) { + $ret .= " '$name' => ". (is_string($value) ? "'$value'" : $value ) . ",\n"; + } + $ret .= " )"; + return $ret; + } + + /** + * Internal method used by compile(). Export required standalone functions. + * + * @param array<string,array|string|integer> $context current compile context + * @param string $code PHP code string of the method + * + * @return array<string|array> list of converted code and children array + */ + protected static function scanLCRunDependency($context, $code) { + $child = array(); + + $code = preg_replace_callback('/self::(\w+?)\s*\(/', function ($matches) use ($context, &$child) { + if (!isset($child[$matches[1]])) { + $child[$matches[1]] = 0; + } + $child[$matches[1]]++; + + return "\$cx['funcs']['{$matches[1]}']("; + }, $code); + + // replace the constants + $code = preg_replace('/self::([A-Z0-9_]+)/', "\$cx['constants']['$1']", $code); + return array($code, $child); + } + + /** + * Internal method used by compile(). Handle exists error and return error status. + * + * @param array<string,array|string|integer> $context Current context of compiler progress. + * + * @throws Exception + * @return boolean True when error detected + * + * @expect true when input array('level' => 1, 'stack' => array('X'), 'flags' => array('errorlog' => 0, 'exception' => 0), 'error' => array()) + * @expect false when input array('level' => 0, 'error' => array()) + * @expect true when input array('level' => 0, 'error' => array('some error'), 'flags' => array('errorlog' => 0, 'exception' => 0)) + */ + protected static function handleError(&$context) { + if ($context['level'] > 0) { + $token = array_pop($context['stack']); + $context['error'][] = "Unclosed token {{{#$token}}} !!"; + } + + static::$lastContext = $context; + + if (count($context['error'])) { + if ($context['flags']['errorlog']) { + error_log(implode("\n", $context['error'])); + } + if ($context['flags']['exception']) { + throw new Exception(implode("\n", $context['error'])); + } + return true; + } + return false; + } + + /** + * Internal method used by compile(). Return 'true' or 'false' string. + * + * @param integer $v value + * + * @return string 'true' when the value larger then 0 + * + * @expect 'true' when input 1 + * @expect 'true' when input 999 + * @expect 'false' when input 0 + * @expect 'false' when input -1 + */ + protected static function getBoolStr($v) { + return ($v > 0) ? 'true' : 'false'; + } + + /** + * Get last compiler context. + * + * @return array<string,array|string|integer> Context data + */ + public static function getContext() { + return static::$lastContext; + } + + /** + * Get a working render function by a string of PHP code. This method may requires php setting allow_url_include=1 and allow_url_fopen=1 , or access right to tmp file system. + * + * @param string $php PHP code + * @param string|null $tmpDir Optional, change temp directory for php include file saved by prepare() when cannot include PHP code with data:// format. + * + * @return Closure|false result of include() + * + * @deprecated + */ + public static function prepare($php, $tmpDir = null) { + if (!ini_get('allow_url_include') || !ini_get('allow_url_fopen')) { + if (!$tmpDir || !is_dir($tmpDir)) { + $tmpDir = sys_get_temp_dir(); + } + } + + if ($tmpDir) { + $fn = tempnam($tmpDir, 'lci_'); + if (!$fn) { + error_log("Can not generate tmp file under $tmpDir!!\n"); + return false; + } + if (!file_put_contents($fn, $php)) { + error_log("Can not include saved temp php code from $fn, you should add $tmpDir into open_basedir!!\n"); + return false; + } + return include($fn); + } + + return include('data://text/plain,' . urlencode($php)); + } + + /** + * Internal method used by compile(). Get function name for standalone or none standalone template. + * + * @param array<string,array|string|integer> $context Current context of compiler progress. + * @param string $name base function name + * @param string $tag original handlabars tag for debug + * + * @return string compiled Function name + * + * @expect 'LCRun3::test(' when input array('flags' => array('standalone' => 0, 'debug' => 0)), 'test', '' + * @expect 'LCRun3::test2(' when input array('flags' => array('standalone' => 0, 'debug' => 0)), 'test2', '' + * @expect "\$cx['funcs']['test3'](" when input array('flags' => array('standalone' => 1, 'debug' => 0)), 'test3', '' + * @expect 'LCRun3::debug(\'abc\', \'test\', ' when input array('flags' => array('standalone' => 0, 'debug' => 1)), 'test', 'abc' + */ + protected static function getFuncName(&$context, $name, $tag) { + static::addUsageCount($context, 'lcrun', $name); + + if ($context['flags']['debug'] && ($name != 'miss')) { + $dbg = "'$tag', '$name', "; + $name = 'debug'; + static::addUsageCount($context, 'lcrun', 'debug'); + } else { + $dbg = ''; + } + + return $context['flags']['standalone'] ? "\$cx['funcs']['$name']($dbg" : "LCRun3::$name($dbg"; + } + + /** + * Internal method used by getArrayCode(). Get variable names translated string. + * + * @param array<string> $scopes an array of variable names with single quote + * + * @return string PHP array names string + * + * @expect '' when input array() + * @expect '[a]' when input array('a') + * @expect '[a][b][c]' when input array('a', 'b', 'c') + */ + protected static function getArrayStr($scopes) { + return count($scopes) ? '[' . implode('][', $scopes) . ']' : ''; + } + + /** + * Internal method used by getVariableName(). Get variable names translated string. + * + * @param array<string> $list an array of variable names. + * + * @return string PHP array names string + * + * @expect '' when input array() + * @expect "['a']" when input array('a') + * @expect "['a']['b']['c']" when input array('a', 'b', 'c') + */ + protected static function getArrayCode($list) { + return static::getArrayStr(array_map(function ($v) { + return "'$v'"; + }, $list)); + } + + /** + * Internal method used by compile(). + * + * @param array<array> $vn variable name array. + * @param array<string,array|string|integer> $context current compile context + * @param boolean $ishelper true when compile for helper + * + * @return array<string|array> variable names + * + * @expect array('array(array($in),array())', array('this')) when input array(null), array('flags'=>array('spvar'=>true)) + * @expect array('array(array($in,$in),array())', array('this', 'this')) when input array(null, null), array('flags'=>array('spvar'=>true)) + * @expect array('array(array(),array(\'a\'=>$in))', array('this')) when input array('a' => null), array('flags'=>array('spvar'=>true)) + */ + protected static function getVariableNames($vn, &$context, $ishelper = false) { + $vars = array(array(), array()); + $exps = array(); + foreach ($vn as $i => $v) { + if (isset($v[0]) && preg_match('/^\(.+\)$/', $v[0])) { + $V = static::compileSubExpression($v[0], $context); + } else { + $V = static::getVariableName($v, $context, $ishelper); + } + if (is_string($i)) { + $vars[1][] = "'$i'=>{$V[0]}"; + } else { + $vars[0][] = $V[0]; + } + $exps[] = $V[1]; + } + return array('array(array(' . implode(',', $vars[0]) . '),array(' . implode(',', $vars[1]) . '))', $exps); + } + + /** + * Internal method used by compile(). + * + * @param string $subExpression subExpression to compile + * @param array<string,array|string|integer> $context current compile context + * + * @return array<string> code representing passed expression + */ + protected static function compileSubExpression($subExpression, &$context) { + // mock up a token for this expression + $token = array_fill(self::POS_LOTHER, self::POS_ROTHER, ''); + + // strip outer ( ) from subexpression + $token[self::POS_INNERTAG] = substr($subExpression, 1, -1); + + list(, $vars) = static::parseTokenArgs($token, $context); + + // no separator is needed, this code will be used as a function argument + $origSeperator = $context['ops']['seperator']; + $context['ops']['seperator'] = ''; + // override $raw, subexpressions are never escaped + $ret = static::compileCustomHelper($context, $vars, true, true); + $context['ops']['seperator'] = $origSeperator; + + return array($ret ? $ret : '', $subExpression); + } + + /** + * Internal method used by compile(). + * + * @param array<array|string|integer> $var variable parsed path + * @param array<array|string|integer> $context current compile context + * @param boolean $ishelper true when compile for helper$ + * + * @return array<string> variable names + * + * @expect array('$in', 'this') when input array(null), array('flags'=>array('spvar'=>true,'debug'=>0)) + * @expect array('true', 'true') when input array('true'), array('flags'=>array('spvar'=>true,'debug'=>0)), true + * @expect array('false', 'false') when input array('false'), array('flags'=>array('spvar'=>true,'debug'=>0)), true + * @expect array(2, '2') when input array('2'), array('flags'=>array('spvar'=>true,'debug'=>0)), true + * @expect array('((isset($in[\'@index\']) && is_array($in)) ? $in[\'@index\'] : null)', '[@index]') when input array('@index'), array('flags'=>array('spvar'=>false,'debug'=>0,'prop'=>0,'method'=>0,'mustlok'=>0)) + * @expect array("((isset(\$cx['sp_vars']['index']) && is_array(\$cx['sp_vars'])) ? \$cx['sp_vars']['index'] : null)", '@[index]') when input array('@index'), array('flags'=>array('spvar'=>true,'debug'=>0)) + * @expect array("((isset(\$cx['sp_vars']['key']) && is_array(\$cx['sp_vars'])) ? \$cx['sp_vars']['key'] : null)", '@[key]') when input array('@key'), array('flags'=>array('spvar'=>true,'debug'=>0)) + * @expect array("((isset(\$cx['sp_vars']['first']) && is_array(\$cx['sp_vars'])) ? \$cx['sp_vars']['first'] : null)", '@[first]') when input array('@first'), array('flags'=>array('spvar'=>true,'debug'=>0)) + * @expect array("((isset(\$cx['sp_vars']['last']) && is_array(\$cx['sp_vars'])) ? \$cx['sp_vars']['last'] : null)", '@[last]') when input array('@last'), array('flags'=>array('spvar'=>true,'debug'=>0)) + * @expect array('\'a\'', '"a"') when input array('"a"'), array('flags'=>array('spvar'=>true,'debug'=>0)) + * @expect array('((isset($in[\'a\']) && is_array($in)) ? $in[\'a\'] : null)', '[a]') when input array('a'), array('flags'=>array('spvar'=>true,'debug'=>0,'prop'=>0,'method'=>0,'mustlok'=>0)) + * @expect array('((isset($cx[\'scopes\'][count($cx[\'scopes\'])-1][\'a\']) && is_array($cx[\'scopes\'][count($cx[\'scopes\'])-1])) ? $cx[\'scopes\'][count($cx[\'scopes\'])-1][\'a\'] : null)', '../[a]') when input array(1,'a'), array('flags'=>array('spvar'=>true,'debug'=>0,'prop'=>0,'method'=>0,'mustlok'=>0)) + * @expect array('((isset($cx[\'scopes\'][count($cx[\'scopes\'])-3][\'a\']) && is_array($cx[\'scopes\'][count($cx[\'scopes\'])-3])) ? $cx[\'scopes\'][count($cx[\'scopes\'])-3][\'a\'] : null)', '../../../[a]') when input array(3,'a'), array('flags'=>array('spvar'=>true,'debug'=>0,'prop'=>0,'method'=>0,'mustlok'=>0)) + * @expect array('((isset($in[\'id\']) && is_array($in)) ? $in[\'id\'] : null)', 'this.[id]') when input array(null, 'id'), array('flags'=>array('spvar'=>true,'debug'=>0,'prop'=>0,'method'=>0,'mustlok'=>0)) + * @expect array('LCRun3::v($cx, $in, array(\'id\'))', 'this.[id]') when input array(null, 'id'), array('flags'=>array('prop'=>true,'spvar'=>true,'debug'=>0,'method'=>0,'mustlok'=>0,'standalone'=>0)) + */ + protected static function getVariableName($var, &$context, $ishelper = false) { + if (isset($var[0])) { + // Handle language constants or number , only for helpers + if ($ishelper) { + if ((count($var) == 1) && is_numeric($var[0])) { + // convert 0x00 or 0b00 numbers to decimal + return array((string) 1 * $var[0], $var[0]); + } + switch ($var[0]) { + case 'true': + return array('true', 'true'); + case 'false': + return array('false', 'false'); + } + } + + // Handle double quoted string + if (preg_match('/^"(.*)"$/', $var[0], $matched)) { + $t = addcslashes(stripslashes(preg_replace('/\\\\\\\\/', '\\', $matched[1])), "'"); + return array("'{$t}'", "\"{$t}\""); + } + } + + $levels = 0; + $base = '$in'; + $spvar = false; + + if (isset($var[0])) { + // trace to parent + if (!is_string($var[0]) && is_int($var[0])) { + $levels = array_shift($var); + } + + // handle @root, @index, @key, @last, etc + if ($context['flags']['spvar']) { + if (substr($var[0], 0, 1) === '@') { + $spvar = true; + $base = "\$cx['sp_vars']"; + $var[0] = substr($var[0], 1); + } + } + + // change base when trace to parent + if ($levels > 0) { + if ($spvar) { + $base .= str_repeat("['_parent']", $levels); + } else { + $base = "\$cx['scopes'][count(\$cx['scopes'])-$levels]"; + } + } + } + + // Generate normalized expression for debug + $exp = static::getExpression($levels, $spvar, $var); + + if ((count($var) == 0) || (is_null($var[0]) && (count($var) == 1))) { + return array($base, $exp); + } + + if (is_null($var[0])) { + array_shift($var); + } + + // 1. To support recursive context lookup... + // 2. To support instance properties or methods... + // the only way is using slower rendering time variable resolver. + if ($context['flags']['prop'] || $context['flags']['method'] || $context['flags']['mustlok']) { + return array(static::getFuncName($context, 'v', $exp) . "\$cx, $base, array(" . implode(',', array_map(function ($V) { + return "'$V'"; + }, $var)) . '))', $exp); + } + + $n = static::getArrayCode($var); + array_pop($var); + $p = count($var) ? static::getArrayCode($var) : ''; + + return array("((isset($base$n) && is_array($base$p)) ? $base$n : " . ($context['flags']['debug'] ? (static::getFuncName($context, 'miss', '') . "\$cx, '$exp')") : 'null' ) . ')', $exp); + } + + /** + * Internal method used by compile(). + * + * @param integer $levels trace N levels top parent scope + * @param boolean $spvar is the path start with @ or not + * @param array<string|integer> $var variable parsed path + * + * @return string normalized expression for debug display + * + * @expect '[a].[b]' when input 0, false, array('a', 'b') + * @expect '@[root]' when input 0, true, array('root') + * @expect 'this' when input 0, false, null + * @expect 'this.[id]' when input 0, false, array(null, 'id') + * @expect '@[root].[a].[b]' when input 0, true, array('root', 'a', 'b') + * @expect '../../[a].[b]' when input 2, false, array('a', 'b') + * @expect '../[a\'b]' when input 1, false, array('a\'b') + */ + protected static function getExpression($levels, $spvar, $var) { + return ($spvar ? '@' : '') . str_repeat('../', $levels) . ((is_array($var) && count($var)) ? implode('.', array_map(function($v) { + return is_null($v) ? 'this' : "[$v]"; + }, $var)) : 'this'); + } + + /** + * Internal method used by compile(). Return array presentation for a variable name + * + * @param string $v variable name to be fixed. + * @param array<string,array|string|integer> $context Current compile content. + * + * @return array<integer,string> Return variable name array + * + * @expect array('this') when input 'this', array('flags' => array('advar' => 0, 'this' => 0)) + * @expect array(null) when input 'this', array('flags' => array('advar' => 0, 'this' => 1)) + * @expect array(1, null) when input '../', array('flags' => array('advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + * @expect array(1, null) when input '../.', array('flags' => array('advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + * @expect array(1, null) when input '../this', array('flags' => array('advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + * @expect array(1, 'a') when input '../a', array('flags' => array('advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + * @expect array(2, 'a', 'b') when input '../../a.b', array('flags' => array('advar' => 0, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + * @expect array(2, '[a]', 'b') when input '../../[a].b', array('flags' => array('advar' => 0, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + * @expect array(2, 'a', 'b') when input '../../[a].b', array('flags' => array('advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + * @expect array('"a.b"') when input '"a.b"', array('flags' => array('advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + * @expect array(null, 'id') when input 'this.id', array('flags' => array('advar' => 1, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + */ + protected static function fixVariable($v, &$context) { + $ret = array(); + $levels = 0; + + // handle double quoted string + if (preg_match('/^"(.*)"$/', $v, $matched)) { + return array($v); + } + + // handle single quoted string + if (preg_match('/^\\\\\'(.*)\\\\\'$/', $v, $matched)) { + return array("\"{$matched[1]}\""); + } + + // handle .. + if ($v === '..') { + $v = '../'; + } + + // Trace to parent for ../ N times + $v = preg_replace_callback('/\\.\\.\\//', function() use (&$levels) { + $levels++; + return ''; + }, trim($v)); + + if ($levels) { + $ret[] = $levels; + if (!$context['flags']['parent']) { + $context['error'][] = 'Do not support {{../var}}, you should do compile with LightnCandy::FLAG_PARENT flag'; + } + $context['usedFeature']['parent']++; + } + + if ($context['flags']['advar'] && preg_match('/\\]/', $v)) { + preg_match_all(self::VARNAME_SEARCH, $v, $matchedall); + } else { + preg_match_all('/([^\\.\\/]+)/', $v, $matchedall); + } + + if (($v === '.') || ($v === '')) { + $matchedall = array(array('.'), array('.')); + } + + foreach ($matchedall[1] as $m) { + if ($context['flags']['advar'] && substr($m, 0, 1) === '[') { + $ret[] = substr($m, 1, -1); + } else { + $ret[] = (($context['flags']['this'] && ($m === 'this')) || ($m === '.')) ? null : $m; + } + } + + return $ret; + } + + /** + * Internal method used by scanFeatures() and compile(). Parse the token and return parsed result. + * + * @param array<string> $token preg_match results + * @param array<string,array|string|integer> $context current compile context + * + * @return array<boolean|array> Return parsed result + * + * @expect array(false, array(array(null))) when input array(0,0,0,0,0,0,''), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + * @expect array(true, array(array(null))) when input array(0,0,0,'{{{',0,0,''), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + * @expect array(true, array(array(null))) when input array(0,0,0,0,0,0,''), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 1)) + * @expect array(false, array(array('a'))) when input array(0,0,0,0,0,0,'a'), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + * @expect array(false, array(array('a'), array('b'))) when input array(0,0,0,0,0,0,'a b'), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + * @expect array(false, array(array('a'), array('"b'), array('c"'))) when input array(0,0,0,0,0,0,'a "b c"'), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + * @expect array(false, array(array('a'), array('"b c"'))) when input array(0,0,0,0,0,0,'a "b c"'), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + * @expect array(false, array(array('a'), array('[b'), array('c]'))) when input array(0,0,0,0,0,0,'a [b c]'), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + * @expect array(false, array(array('a'), array('[b'), array('c]'))) when input array(0,0,0,0,0,0,'a [b c]'), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + * @expect array(false, array(array('a'), array('b c'))) when input array(0,0,0,0,0,0,'a [b c]'), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + * @expect array(false, array(array('a'), array('b c'))) when input array(0,0,0,0,0,0,'a [b c]'), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + * @expect array(false, array(array('a'), 'q' => array('b c'))) when input array(0,0,0,0,0,0,'a q=[b c]'), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + * @expect array(false, array(array('a'), array('q=[b c'))) when input array(0,0,0,0,0,0,'a [q=[b c]'), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + * @expect array(false, array(array('a'), 'q' => array('[b'), array('c]'))) when input array(0,0,0,0,0,0,'a q=[b c]'), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + * @expect array(false, array(array('a'), 'q' => array('b'), array('c'))) when input array(0,0,0,0,0,0,'a [q]=b c'), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + * @expect array(false, array(array('a'), 'q' => array('"b c"'))) when input array(0,0,0,0,0,0,'a q="b c"'), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + * @expect array(false, array(array('(foo bar)'))) when input array(0,0,0,0,0,0,'(foo bar)'), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + * @expect array(false, array(array('foo'), array("'=='"), array('bar'))) when input array(0,0,0,0,0,0,"foo '==' bar"), array('flags' => array('advar' => 1, 'namev' => 1)) + */ + protected static function parseTokenArgs(&$token, &$context) { + trim($token[self::POS_INNERTAG]); + + // Handle delimiter change + if (preg_match('/^=\s*([^ ]+)\s+([^ ]+)\s*=$/', $token[self::POS_INNERTAG], $matched)) { + static::setupToken($context, $matched[1], $matched[2]); + $token[self::POS_OP] = ' '; + return array(false, array()); + } + + $vars = array(); + $count = preg_match_all('/(\s*)([^\s]+)/', $token[self::POS_INNERTAG], $matchedall); + + // Parse arguments and deal with "..." or [...] or (...) + if (($count > 0) && $context['flags']['advar']) { + $prev = ''; + $expect = 0; + foreach ($matchedall[2] as $index => $t) { + // continue from previous match when expect something + if ($expect) { + $prev .= "{$matchedall[1][$index]}$t"; + // end an argument when end with expected charactor + if (substr($t, -1, 1) === $expect) { + $vars[] = $prev; + $prev = ''; + $expect = 0; + } + continue; + } + + // continue to next match when begin with '(' without ending ')' + if (preg_match('/^\([^\)]+$/', $t)) { + $prev = $t; + $expect = ')'; + continue; + } + + // continue to next match when begin with '"' without ending '"' + if (preg_match('/^"[^"]+$/', $t)) { + $prev = $t; + $expect = '"'; + continue; + } + + // continue to next match when '="' exists without ending '"' + if (preg_match('/="[^"]+$/', $t)) { + $prev = $t; + $expect = '"'; + continue; + } + + // continue to next match when begin with \' without ending ' + if (preg_match('/^\\\\\'[^\']+$/', $t)) { + $prev = $t; + $expect = '\''; + continue; + } + + // continue to next match when =\' exists without ending ' + if (preg_match('/=\\\\\'[^\']+$/', $t)) { + $prev = $t; + $expect = '\''; + continue; + } + + // continue to next match when '[' exists without ending ']' + if (preg_match('/\\[[^\\]]+$/', $t)) { + $prev = $t; + $expect = ']'; + continue; + } + $vars[] = $t; + } + } else { + $vars = ($count > 0) ? $matchedall[2] : explode(' ', $token[self::POS_INNERTAG]); + } + + // Check for advanced variable. + $ret = array(); + $i = 0; + foreach ($vars as $idx => $var) { + // Skip advanced processing for subexpressions + if (preg_match('/^\(.+\)$/', $var)) { + $ret[$i] = array($var); + $i++; + continue; + } + + if ($context['flags']['namev']) { + if (preg_match('/^((\\[([^\\]]+)\\])|([^=^["\']+))=(.+)$/', $var, $m)) { + if (!$context['flags']['advar'] && $m[3]) { + $context['error'][] = "Wrong argument name as '[$m[3]]' in " . static::tokenString($token) . ' ! You should fix your template or compile with LightnCandy::FLAG_ADVARNAME flag.'; + } + $idx = $m[3] ? $m[3] : $m[4]; + $var = $m[5]; + } + } + if ($context['flags']['advar']) { + // foo] Rule 1: no starting [ or [ not start from head + if (preg_match('/^[^\\[\\.]+[\\]\\[]/', $var) + // [bar Rule 2: no ending ] or ] not in the end + || preg_match('/[\\[\\]][^\\]\\.]+$/', $var) + // ]bar. Rule 3: middle ] not before . + || preg_match('/\\][^\\]\\[\\.]+\\./', $var) + // .foo[ Rule 4: middle [ not after . + || preg_match('/\\.[^\\]\\[\\.]+\\[/', preg_replace('/^(..\\/)+/', '', preg_replace('/\\[[^\\]]+\\]/', '[XXX]', $var))) + ) { + $context['error'][] = "Wrong variable naming as '$var' in " . static::tokenString($token) . ' !'; + } + } + + if (($idx === 0) && ($token[self::POS_OP] === '>')) { + $var = array(preg_replace('/^("(.+)")|(\\[(.+)\\])$/', '$2$4', $var)); + } else if (is_numeric($var)) { + $var = array('"' . $var . '"'); + } else { + $var = static::fixVariable($var, $context); + } + + if (is_string($idx)) { + $ret[$idx] = $var; + } else { + $ret[$i] = $var; + $i++; + } + } + + return array(($token[self::POS_BEGINTAG] === '{{{') || ($token[self::POS_OP] === '&') || $context['flags']['noesc'], $ret); + } + + /** + * Internal method used by scanFeatures(). return token string + * + * @param string[] $token detected handlebars {{ }} token + * @param integer $remove remove how many heading and ending token + * + * @return string Return whole token + * + * @expect 'b' when input array(0, 'a', 'b', 'c'), 1 + * @expect 'c' when input array(0, 'a', 'b', 'c', 'd', 'e') + */ + protected static function tokenString($token, $remove = 2) { + return implode('', array_slice($token, 1 + $remove, -$remove)); + } + + /** + * Internal method used by scanFeatures(). Validate start and and. + * + * @param string[] $token detected handlebars {{ }} token + * @param array<string,array|string|integer> $context current compile context + * + * @return boolean|null Return true when invalid + * + * @expect null when input array_fill(0, 9, ''), array() + * @expect null when input array_fill(0, 9, '}}'), array() + * @expect true when input array_fill(0, 9, '{{{'), array() + */ + protected static function validateStartEnd($token, &$context) { + // {{ }}} or {{{ }} are invalid + if (strlen($token[self::POS_BEGINTAG]) !== strlen($token[self::POS_ENDTAG])) { + $context['error'][] = 'Bad token ' . static::tokenString($token) . ' ! Do you mean {{' . static::tokenString($token, 4) . '}} or {{{' . static::tokenString($token, 4) . '}}}?'; + return true; + } + // {{{# }}} or {{{! }}} or {{{/ }}} or {{{^ }}} are invalid. + if ((strlen($token[self::POS_BEGINTAG]) === 3) && $token[self::POS_OP] && ($token[self::POS_OP] !== '&')) { + $context['error'][] = 'Bad token ' . static::tokenString($token) . ' ! Do you mean {{' . static::tokenString($token, 4) . '}} ?'; + return true; + } + } + + /** + * Internal method used by compile(). Collect handlebars usage information, detect template error. + * + * @param string[] $token detected handlebars {{ }} token + * @param array<string,array|string|integer> $context current compile context + * @param array<array> $vars parsed arguments list + * + * @return boolean|integer|null Return true when invalid or detected + * + * @expect null when input array(0, 0, 0, 0, 0, ''), array(), array() + * @expect 2 when input array(0, 0, 0, 0, 0, '^', '...'), array('usedFeature' => array('isec' => 1), 'level' => 0), array(array('foo')) + * @expect 3 when input array(0, 0, 0, 0, 0, '!', '...'), array('usedFeature' => array('comment' => 2)), array() + * @expect true when input array(0, 0, 0, 0, 0, '/'), array('stack' => array(1), 'level' => 1), array() + * @expect 4 when input array(0, 0, 0, 0, 0, '#', '...'), array('usedFeature' => array('sec' => 3), 'level' => 0), array(array('x')) + * @expect 5 when input array(0, 0, 0, 0, 0, '#', '...'), array('usedFeature' => array('if' => 4), 'level' => 0), array(array('if')) + * @expect 6 when input array(0, 0, 0, 0, 0, '#', '...'), array('usedFeature' => array('with' => 5), 'level' => 0, 'flags' => array('with' => 1)), array(array('with')) + * @expect 7 when input array(0, 0, 0, 0, 0, '#', '...'), array('usedFeature' => array('each' => 6), 'level' => 0), array(array('each')) + * @expect 8 when input array(0, 0, 0, 0, 0, '#', '...'), array('usedFeature' => array('unless' => 7), 'level' => 0), array(array('unless')) + * @expect 9 when input array(0, 0, 0, 0, 0, '#', '...'), array('blockhelpers' => array('abc' => ''), 'usedFeature' => array('bhelper' => 8), 'level' => 0), array(array('abc')) + * @expect 10 when input array(0, 0, 0, 0, 0, ' ', '...'), array('usedFeature' => array('delimiter' => 9), 'level' => 0), array() + * @expect 11 when input array(0, 0, 0, 0, 0, '#', '...'), array('hbhelpers' => array('abc' => ''), 'usedFeature' => array('hbhelper' => 10), 'level' => 0), array(array('abc')) + * @expect true when input array(0, 0, 0, 0, 0, '>', '...'), array('basedir' => array('.'), 'fileext' => array('.tmpl'), 'usedFeature' => array('unless' => 7, 'partial' => 7), 'level' => 0, 'flags' => array('skippartial' => 0)), array('test') + */ + protected static function validateOperations($token, &$context, $vars) { + switch ($token[self::POS_OP]) { + case '>': + static::readPartial($vars[0][0], $context); + return true; + + case ' ': + return ++$context['usedFeature']['delimiter']; + + case '^': + if ($vars[0][0]) { + $context['stack'][] = $token[self::POS_INNERTAG]; + $context['level']++; + return ++$context['usedFeature']['isec']; + } + + if (!$context['flags']['else']) { + $context['error'][] = 'Do not support {{^}}, you should do compile with LightnCandy::FLAG_ELSE flag'; + } + return; + + case '/': + array_pop($context['stack']); + $context['level']--; + return true; + + case '!': + return ++$context['usedFeature']['comment']; + + case '#': + $context['stack'][] = $token[self::POS_INNERTAG]; + $context['level']++; + + // detect handlebars custom helpers. + if (isset($context['hbhelpers'][$vars[0][0]])) { + return ++$context['usedFeature']['hbhelper']; + } + + // detect block custom helpers. + if (isset($context['blockhelpers'][$vars[0][0]])) { + return ++$context['usedFeature']['bhelper']; + } + + switch ($vars[0][0]) { + case 'with': + if ($context['flags']['with']) { + if (count($vars) < 2) { + $context['error'][] = 'No argument after {{#with}} !'; + } + } else { + if (isset($vars[1][0])) { + $context['error'][] = 'Do not support {{#with var}}, you should do compile with LightnCandy::FLAG_WITH flag'; + } + } + // Continue to add usage... + case 'each': + case 'unless': + case 'if': + return ++$context['usedFeature'][$vars[0][0]]; + + default: + return ++$context['usedFeature']['sec']; + } + } + } + + /** + * Internal method used by compile(). Collect handlebars usage information, detect template error. + * + * @param string[] $token detected handlebars {{ }} token + * @param array<string,array|string|integer> $context current compile context + */ + protected static function scanFeatures($token, &$context) { + list($raw, $vars) = static::parseTokenArgs($token, $context); + + if (static::validateStartEnd($token, $context)) { + return; + } + + if (static::validateOperations($token, $context, $vars)) { + return; + } + + if (($token[self::POS_OP] === '^') && ($context['flags']['else'])) { + return $context['usedFeature']['else']++; + } + + if (count($vars) == 0) { + return $context['error'][] = 'Wrong variable naming in ' . static::tokenString($token); + } + + if (!isset($vars[0])) { + return static::noNamedArguments($token, $context, true, ', you should use it after a custom helper.'); + } + + if ($vars[0] !== 'else') { + $context['usedFeature'][$raw ? 'raw' : 'enc']++; + } + + // validate else and this. + switch ($vars[0][0]) { + case 'else': + if ($context['flags']['else']) { + return $context['usedFeature']['else']++; + } + break; + + case 'this': + case '.': + if ($context['level'] == 0) { + $context['usedFeature']['rootthis']++; + } + return $context['usedFeature'][($vars[0] == '.') ? 'dot' : 'this']++; + } + + // detect handlebars custom helpers. + if (isset($context['hbhelpers'][$vars[0][0]])) { + return $context['usedFeature']['hbhelper']++; + } + + // detect custom helpers. + if (isset($context['helpers'][$vars[0][0]])) { + return $context['usedFeature']['helper']++; + } + } + + /** + * Internal method used by compile(). Show error message when named arguments appear without custom helper. + * + * @param array<string> $token detected handlebars {{ }} token + * @param array<string,array|string|integer> $context current compile context + * @param boolean $named is named arguments + * @param string $suggest extended hint for this no named argument error + */ + public static function noNamedArguments($token, &$context, $named, $suggest = '!') { + if ($named) { + $context['error'][] = 'Do not support name=value in ' . static::tokenString($token) . $suggest; + } + } + + /** + * Internal method used by compileToken(). Modify $token when spacing rules matched. + * + * @param array<string> $token detected handlebars {{ }} token + * @param array<array|string|integer> $vars parsed arguments list + * @param array<string,array|string|integer> $context current compile context + * + * @return string|null Return compiled code segment for the token + */ + public static function handleMustacheSpacing(&$token, $vars, &$context) { + if (!$context['flags']['mustsp'] && !$context['flags']['mustpi']) { + return; + } + + // left line change detection + $lsp = preg_match('/^(.*)(\\r?\\n)([ \\t]*?)$/s', $token[self::POS_LSPACE], $lmatch); + $ind = $lsp ? $lmatch[3] : $token[self::POS_LSPACE]; + + // right line change detection + $rsp = preg_match('/^([ \\t]*?)(\\r?\\n)(.*)$/s', $token[self::POS_RSPACE], $rmatch); + $st = true; + + // setup ahead flag + $ahead = $context['tokens']['ahead']; + $context['tokens']['ahead'] = preg_match('/^[^\n]*{{/s', $token[self::POS_RSPACE] . $token[self::POS_ROTHER]); + + // reset partial indent + $context['tokens']['partialind'] = ''; + + // same tags in the same line , not standalone + if (!$lsp && $ahead) { + $st = false; + } + + // Do not need standalone detection for these tags + if (!$token[self::POS_OP] || ($token[self::POS_OP] === '&')) { + if (!$context['flags']['else'] || (isset($vars[0][0]) && ($vars[0][0] !== 'else'))) { + $st = false; + } + } + + // not standalone because other things in the same line ahead + if ($token[self::POS_LOTHER] && !$token[self::POS_LSPACE]) { + $st = false; + } + + // not standalone because other things in the same line behind + if ($token[self::POS_ROTHER] && !$token[self::POS_RSPACE]) { + $st = false; + } + + if ($st && (($lsp && $rsp) // both side cr + || ($rsp && !$token[self::POS_LOTHER]) // first line without left + || ($lsp && ($context['tokens']['current'] == $context['tokens']['count']) && !$token[self::POS_ROTHER]) // final line + )) { + // handle partial + if ($context['flags']['mustpi'] && ($token[self::POS_OP] === '>')) { + $context['tokens']['partialind'] = $ind; + } + if ($context['flags']['mustsp']) { + $token[self::POS_LSPACE] = (isset($lmatch[2]) ? ($lmatch[1] . $lmatch[2]) : ''); + $token[self::POS_RSPACE] = isset($rmatch[3]) ? $rmatch[3] : ''; + } + } + } + + /** + * Internal method used by compile(). Return compiled PHP code partial for a handlebars token. + * + * @param array<string> $token detected handlebars {{ }} token + * @param array<string,array|string|integer> $context current compile context + * + * @return string Return compiled code segment for the token + */ + public static function compileToken(&$token, &$context) { + list($raw, $vars) = static::parseTokenArgs($token, $context); + $named = count(array_diff_key($vars, array_keys(array_keys($vars)))) > 0; + + // Handle spacing (standalone tags, partial indent) + static::handleMustacheSpacing($token, $vars, $context); + + // Handle space control. + if ($token[self::POS_LSPACECTL]) { + $token[self::POS_LSPACE] = ''; + } + + if ($token[self::POS_RSPACECTL]) { + $token[self::POS_RSPACE] = ''; + } + + if ($ret = static::compileSection($token, $context, $vars, $named)) { + return $ret; + } + + if ($ret = static::compileCustomHelper($context, $vars, $raw)) { + return $ret; + } + + if ($ret = static::compileElse($context, $vars)) { + return $ret; + } + + static::noNamedArguments($token, $context, $named, ', maybe you missing the custom helper?'); + + return static::compileVariable($context, $vars, $raw); + } + + /** + * Internal method used by compile(). Return compiled PHP code partial for a handlebars section token. + * + * @param array<string> $token detected handlebars {{ }} token + * @param array<string,array|string|integer> $context current compile context + * @param array<array|string|integer> $vars parsed arguments list + * @param boolean $named is named arguments or not + * + * @return string|null Return compiled code segment for the token when the token is section + */ + protected static function compileSection(&$token, &$context, &$vars, $named) { + switch ($token[self::POS_OP]) { + case '>': + // mustache spec: ignore missing partial + if (!isset($context['usedPartial'][$vars[0][0]])) { + return $context['ops']['seperator']; + } + $p = array_shift($vars); + if (!isset($vars[0])) { + $vars[0] = array(); + } + $v = static::getVariableNames($vars, $context, true); + $tag = ">$p[0] " .implode(' ', $v[1]); + if ($context['flags']['runpart']) { + $sp = $context['tokens']['partialind'] ? ", '{$context['tokens']['partialind']}'" : ''; + return $context['ops']['seperator'] . static::getFuncName($context, 'p', $tag) . "\$cx, '$p[0]', $v[0]$sp){$context['ops']['seperator']}"; + } else { + if ($named || $v[0] !== 'array(array($in),array())') { + $context['error'][] = "Do not support {{{$tag}}}, you should do compile with LightnCandy::FLAG_RUNTIMEPARTIAL flag"; + } + return "{$context['ops']['seperator']}'" . static::compileTemplate($context, preg_replace('/^/m', $context['tokens']['partialind'], $context['usedPartial'][$p[0]]), $p[0]) . "'{$context['ops']['seperator']}"; + } + case '^': + // {{^}} means {{else}} + if (!$vars[0][0]) { + $vars[0][0] = 'else'; + $token[self::POS_OP] = ''; + return; + } + + // Try to compile as custom helper {{^myHelper}} + $r = static::compileBlockCustomHelper($context, $vars, true); + if ($r) { + return $r; + } + + $v = static::getVariableName($vars[0], $context); + $context['stack'][] = $v[1]; + $context['stack'][] = '^'; + static::noNamedArguments($token, $context, $named); + // Compile to inverted section {{^myVar}} + return "{$context['ops']['cnd_start']}(" . static::getFuncName($context, 'isec', '^' . $v[1]) . "\$cx, {$v[0]})){$context['ops']['cnd_then']}"; + case '/': + return static::compileBlockEnd($token, $context, $vars); + case '!': + case ' ': + return $context['ops']['seperator']; + case '#': + // Try to compile as custom helper {{#myHelper}} + $r = static::compileBlockCustomHelper($context, $vars); + if ($r) { + return $r; + } + static::noNamedArguments($token, $context, $named, ', maybe you missing the block custom helper?'); + // Compile to section {{#myVar}} + return static::compileBlockBegin($context, $vars); + } + } + + /** + * Internal method used by compile(). Return compiled PHP code partial for a handlebars block custom helper begin token. + * + * @param array<string,array|string|integer> $context current compile context + * @param array<array|string|integer> $vars parsed arguments list + * @param boolean $inverted the logic will be inverted + * + * @return string|null Return compiled code segment for the token + */ + protected static function compileBlockCustomHelper(&$context, $vars, $inverted = false) { + $notHBCH = !isset($context['hbhelpers'][$vars[0][0]]); + + if (!isset($context['blockhelpers'][$vars[0][0]]) && $notHBCH) { + return; + } + + $v = static::getVariableName($vars[0], $context); + $context['stack'][] = $v[1]; + $context['stack'][] = '#'; + $ch = array_shift($vars); + $inverted = $inverted ? 'true' : 'false'; + + static::addUsageCount($context, $notHBCH ? 'blockhelpers' : 'hbhelpers', $ch[0]); + $v = static::getVariableNames($vars, $context, true); + return $context['ops']['seperator'] . static::getFuncName($context, $notHBCH ? 'bch' : 'hbch', ($inverted ? '^' : '#') . implode(' ', $v[1])) . "\$cx, '$ch[0]', {$v[0]}, \$in, $inverted, function(\$cx, \$in) {{$context['ops']['f_start']}"; + } + + /** + * Internal method used by compile(). Return compiled PHP code partial for a handlebars block end token. + * + * @param array<string> $token detected handlebars {{ }} token + * @param array<string,array|string|integer> $context current compile context + * @param array<array|string|integer> $vars parsed arguments list + * + * @return string Return compiled code segment for the token + */ + protected static function compileBlockEnd(&$token, &$context, $vars) { + $each = false; + $pop = array_pop($context['stack']); + switch ($token[self::POS_INNERTAG]) { + case 'if': + case 'unless': + if ($pop == ':') { + array_pop($context['stack']); + return $context['usedFeature']['parent'] ? "{$context['ops']['f_end']}}){$context['ops']['seperator']}" : "{$context['ops']['cnd_end']}"; + } + return $context['usedFeature']['parent'] ? "{$context['ops']['f_end']}}){$context['ops']['seperator']}" : "{$context['ops']['cnd_else']}''{$context['ops']['cnd_end']}"; + case 'with': + if ($context['flags']['with']) { + if ($pop !== 'with') { + $context['error'][] = 'Unexpect token: {{/with}} !'; + return; + } + return "{$context['ops']['f_end']}}){$context['ops']['seperator']}"; + } + break; + case 'each': + $each = true; + } + + switch($pop) { + case '#': + case '^': + $pop2 = array_pop($context['stack']); + $v = static::getVariableName($vars[0], $context); + if (!$each && ($pop2 !== $v[1])) { + $context['error'][] = 'Unexpect token ' . static::tokenString($token) . " ! Previous token {{{$pop}$pop2}} is not closed"; + return; + } + if ($pop == '^') { + return "{$context['ops']['cnd_else']}''{$context['ops']['cnd_end']}"; + } + return "{$context['ops']['f_end']}}){$context['ops']['seperator']}"; + default: + $context['error'][] = 'Unexpect token: ' . static::tokenString($token) . ' !'; + return; + } + } + + /** + * Internal method used by compile(). Return compiled PHP code partial for a handlebars block begin token. + * + * @param array<string,array|string|integer> $context current compile context + * @param array<array|string|integer> $vars parsed arguments list + * + * @return string Return compiled code segment for the token + */ + protected static function compileBlockBegin(&$context, $vars) { + $each = 'false'; + $v = isset($vars[1]) ? static::getVariableName($vars[1], $context, true) : array(null, array()); + switch ($vars[0][0]) { + case 'if': + $context['stack'][] = 'if'; + return $context['usedFeature']['parent'] + ? $context['ops']['seperator'] . static::getFuncName($context, 'ifv', 'if ' . $v[1]) . "\$cx, {$v[0]}, \$in, function(\$cx, \$in) {{$context['ops']['f_start']}" + : "{$context['ops']['cnd_start']}(" . static::getFuncName($context, 'ifvar', $v[1]) . "\$cx, {$v[0]})){$context['ops']['cnd_then']}"; + case 'unless': + $context['stack'][] = 'unless'; + return $context['usedFeature']['parent'] + ? $context['ops']['seperator'] . static::getFuncName($context, 'unl', 'unless ' . $v[1]) . "\$cx, {$v[0]}, \$in, function(\$cx, \$in) {{$context['ops']['f_start']}" + : "{$context['ops']['cnd_start']}(!" . static::getFuncName($context, 'ifvar', $v[1]) . "\$cx, {$v[0]})){$context['ops']['cnd_then']}"; + case 'each': + $each = 'true'; + array_shift($vars); + if (!isset($vars[0])) { + $vars[0] = array(null); + } + break; + case 'with': + if ($context['flags']['with']) { + $context['stack'][] = 'with'; + return $context['ops']['seperator'] . static::getFuncName($context, 'wi', 'with ' . $v[1]) . "\$cx, {$v[0]}, \$in, function(\$cx, \$in) {{$context['ops']['f_start']}"; + } + } + + $v = static::getVariableName($vars[0], $context); + $context['stack'][] = $v[1]; + $context['stack'][] = '#'; + return $context['ops']['seperator'] . static::getFuncName($context, 'sec', (($each == 'true') ? 'each ' : '') . $v[1]) . "\$cx, {$v[0]}, \$in, $each, function(\$cx, \$in) {{$context['ops']['f_start']}"; + } + + /** + * Internal method used by compile(). Return compiled PHP code partial for a handlebars custom helper token. + * + * @param array<string,array|string|integer> $context current compile context + * @param array<array|string|integer> $vars parsed arguments list + * @param boolean $raw is this {{{ token or not + * @param boolean $err should cause error when missing helper or not + * + * @return string|null Return compiled code segment for the token when the token is custom helper + */ + protected static function compileCustomHelper(&$context, &$vars, $raw, $err = false) { + $notHH = !isset($context['hbhelpers'][$vars[0][0]]); + if (!isset($context['helpers'][$vars[0][0]]) && $notHH) { + if ($err) { + $context['error'][] = "Custom helper '{$vars[0][0]}' not found!"; + } + return; + } + + $fn = $raw ? 'raw' : $context['ops']['enc']; + $ch = array_shift($vars); + $v = static::getVariableNames($vars, $context, true); + static::addUsageCount($context, $notHH ? 'helpers' : 'hbhelpers', $ch[0]); + return $context['ops']['seperator'] . static::getFuncName($context, $notHH ? 'ch' : 'hbch', "$ch[0] " . implode(' ', $v[1])) . "\$cx, '$ch[0]', {$v[0]}, '$fn'" . ($notHH ? '' : ', \'$in\'') . "){$context['ops']['seperator']}"; + } + + /** + * Internal method used by compile(). Return compiled PHP code partial for a handlebars else token. + * + * @param array<string,array|string|integer> $context current compile context + * @param array<array|string|integer> $vars parsed arguments list + * + * @return string|null Return compiled code segment for the token when the token is else + */ + protected static function compileElse(&$context, &$vars) { + if ($vars[0][0] === 'else') { + $c = count($context['stack']) - 1; + if ($c >= 0) { + switch ($context['stack'][count($context['stack']) - 1]) { + case 'if': + case 'unless': + $context['stack'][] = ':'; + return $context['usedFeature']['parent'] ? "{$context['ops']['f_end']}}, function(\$cx, \$in) {{$context['ops']['f_start']}" : "{$context['ops']['cnd_else']}"; + case 'with': + case 'each': + case '#': + return "{$context['ops']['f_end']}}, function(\$cx, \$in) {{$context['ops']['f_start']}"; + } + } + $context['error'][] = '{{else}} only valid in if, unless, each, and #section context'; + } + } + + /** + * Internal method used by compile(). Return compiled PHP code partial for a handlebars variable token. + * + * @param array<string,array|string|integer> $context current compile context + * @param array<array|string|integer> $vars parsed arguments list + * @param boolean $raw is this {{{ token or not + * + * @return string Return compiled code segment for the token + */ + protected static function compileVariable(&$context, &$vars, $raw) { + $v = static::getVariableName($vars[0], $context); + if ($context['flags']['jsobj'] || $context['flags']['jstrue'] || $context['flags']['debug']) { + return $context['ops']['seperator'] . static::getFuncName($context, $raw ? 'raw' : $context['ops']['enc'], $v[1]) . "\$cx, {$v[0]}){$context['ops']['seperator']}"; + } else { + return $raw ? "{$context['ops']['seperator']}$v[0]{$context['ops']['seperator']}" : "{$context['ops']['seperator']}htmlentities((string){$v[0]}, ENT_QUOTES, 'UTF-8'){$context['ops']['seperator']}"; + } + } + + /** + * Internal method used by compile(). Add usage count to context + * + * @param array<string,array|string|integer> $context current context + * @param string $category ctegory name, can be one of: 'var', 'helpers', 'blockhelpers' + * @param string $name used name + * @param integer $count increment + * + * @expect 1 when input array('usedCount' => array('test' => array())), 'test', 'testname' + * @expect 3 when input array('usedCount' => array('test' => array('testname' => 2))), 'test', 'testname' + * @expect 5 when input array('usedCount' => array('test' => array('testname' => 2))), 'test', 'testname', 3 + */ + protected static function addUsageCount(&$context, $category, $name, $count = 1) { + if (!isset($context['usedCount'][$category][$name])) { + $context['usedCount'][$category][$name] = 0; + } + return ($context['usedCount'][$category][$name] += $count); + } +} + +/** + * LightnCandy static class for compiled template runtime methods. + */ +class LCRun3 { + const DEBUG_ERROR_LOG = 1; + const DEBUG_ERROR_EXCEPTION = 2; + const DEBUG_TAGS = 4; + const DEBUG_TAGS_ANSI = 12; + const DEBUG_TAGS_HTML = 20; + + /** + * LightnCandy runtime method for output debug info. + * + * @param string $v expression + * @param string $f runtime function name + * @param array<string,array|string|integer> $cx render time context + * + * @expect '{{123}}' when input '123', 'miss', array('flags' => array('debug' => LCRun3::DEBUG_TAGS)), '' + * @expect '<!--MISSED((-->{{#123}}<!--))--><!--SKIPPED--><!--MISSED((-->{{/123}}<!--))-->' when input '123', 'wi', array('flags' => array('debug' => LCRun3::DEBUG_TAGS_HTML)), false, false, function () {return 'A';} + */ + public static function debug($v, $f, $cx) { + $params = array_slice(func_get_args(), 2); + $r = call_user_func_array((isset($cx['funcs'][$f]) ? $cx['funcs'][$f] : "LCRun3::$f"), $params); + + if ($cx['flags']['debug'] & self::DEBUG_TAGS) { + $ansi = $cx['flags']['debug'] & (self::DEBUG_TAGS_ANSI - self::DEBUG_TAGS); + $html = $cx['flags']['debug'] & (self::DEBUG_TAGS_HTML - self::DEBUG_TAGS); + $cs = ($html ? (($r !== '') ? '<!!--OK((-->' : '<!--MISSED((-->') : '') + . ($ansi ? (($r !== '') ? "\033[0;32m" : "\033[0;31m") : ''); + $ce = ($html ? '<!--))-->' : '') + . ($ansi ? "\033[0m" : ''); + switch ($f) { + case 'sec': + case 'ifv': + case 'unl': + case 'wi': + if ($r == '') { + if ($ansi) { + $r = "\033[0;33mSKIPPED\033[0m"; + } + if ($html) { + $r = '<!--SKIPPED-->'; + } + } + return "$cs{{#{$v}}}$ce{$r}$cs{{/{$v}}}$ce"; + default: + return "$cs{{{$v}}}$ce"; + } + } else { + return $r; + } + } + + /** + * LightnCandy runtime method for missing data error. + * + * @param array<string,array|string|integer> $cx render time context + * @param string $v expression + */ + public static function miss($cx, $v) { + $e = "LCRun3: $v is not exist"; + if ($cx['flags']['debug'] & self::DEBUG_ERROR_LOG) { + error_log($e); + return; + } + if ($cx['flags']['debug'] & self::DEBUG_ERROR_EXCEPTION) { + throw new Exception($e); + } + } + + /** + * LightnCandy runtime method for variable lookup. It is slower and only be used for instance property or method detection. + * + * @param array<string,array|string|integer> $cx render time context + * @param array<array|string|integer> $base current variable context + * @param array<string|integer> $path array of names for path + * + * @return null|string Return the value or null when not found + * + * @expect null when input array('scopes' => array(), 'flags' => array('prop' => 0, 'method' => 0, 'mustlok' => 0)), 0, array('a', 'b') + * @expect 3 when input array('scopes' => array(), 'flags' => array('prop' => 0, 'method' => 0), 'mustlok' => 0), array('a' => array('b' => 3)), array('a', 'b') + * @expect null when input array('scopes' => array(), 'flags' => array('prop' => 0, 'method' => 0, 'mustlok' => 0)), (Object) array('a' => array('b' => 3)), array('a', 'b') + * @expect 3 when input array('scopes' => array(), 'flags' => array('prop' => 1, 'method' => 0, 'mustlok' => 0)), (Object) array('a' => array('b' => 3)), array('a', 'b') + */ + public static function v($cx, $base, $path) { + $count = count($cx['scopes']); + while ($base) { + $v = $base; + foreach ($path as $name) { + if (is_array($v) && isset($v[$name])) { + $v = $v[$name]; + continue; + } + if (is_object($v)) { + if ($cx['flags']['prop'] && isset($v->$name)) { + $v = $v->$name; + continue; + } + if ($cx['flags']['method'] && is_callable(array($v, $name))) { + $v = $v->$name(); + continue; + } + } + if ($cx['flags']['mustlok']) { + unset($v); + break; + } + return null; + } + if (isset($v)) { + return $v; + } + $count--; + if ($count >= 0) { + $base = $cx['scopes'][$count]; + } else { + return null; + } + } + } + + /** + * LightnCandy runtime method for {{#if var}}. + * + * @param array<string,array|string|integer> $cx render time context + * @param array<array|string|integer>|string|integer|null $v value to be tested + * + * @return boolean Return true when the value is not null nor false. + * + * @expect false when input array(), null + * @expect false when input array(), 0 + * @expect false when input array(), false + * @expect true when input array(), true + * @expect true when input array(), 1 + * @expect false when input array(), '' + * @expect false when input array(), array() + * @expect true when input array(), array('') + * @expect true when input array(), array(0) + */ + public static function ifvar($cx, $v) { + return !is_null($v) && ($v !== false) && ($v !== 0) && ($v !== '') && (is_array($v) ? (count($v) > 0) : true); + } + + /** + * LightnCandy runtime method for {{#if var}} when {{../var}} used. + * + * @param array<string,array|string|integer> $cx render time context + * @param array<array|string|integer>|string|integer|null $v value to be tested + * @param array<array|string|integer> $in input data with current scope + * @param Closure|null $truecb callback function when test result is true + * @param Closure|null $falsecb callback function when test result is false + * + * @return string The rendered string of the section + * + * @expect '' when input array('scopes' => array()), null, array(), null + * @expect '' when input array('scopes' => array()), null, array(), function () {return 'Y';} + * @expect 'Y' when input array('scopes' => array()), 1, array(), function () {return 'Y';} + * @expect 'N' when input array('scopes' => array()), null, array(), function () {return 'Y';}, function () {return 'N';} + */ + public static function ifv($cx, $v, $in, $truecb, $falsecb = null) { + $ret = ''; + if (self::ifvar($cx, $v)) { + if ($truecb) { + $cx['scopes'][] = $in; + $ret = $truecb($cx, $in); + array_pop($cx['scopes']); + } + } else { + if ($falsecb) { + $cx['scopes'][] = $in; + $ret = $falsecb($cx, $in); + array_pop($cx['scopes']); + } + } + return $ret; + } + + /** + * LightnCandy runtime method for {{#unless var}} when {{../var}} used. + * + * @param array<string,array|string|integer> $cx render time context + * @param array<array|string|integer>|string|integer|null $var value be tested + * @param array<array|string|integer>|string|integer|null $in input data with current scope + * @param Closure $truecb callback function when test result is true + * @param Closure|null $falsecb callback function when test result is false + * + * @return string Return rendered string when the value is not null nor false. + * + * @expect '' when input array('scopes' => array()), null, array(), null + * @expect 'Y' when input array('scopes' => array()), null, array(), function () {return 'Y';} + * @expect '' when input array('scopes' => array()), 1, array(), function () {return 'Y';} + * @expect 'Y' when input array('scopes' => array()), null, array(), function () {return 'Y';}, function () {return 'N';} + * @expect 'N' when input array('scopes' => array()), true, array(), function () {return 'Y';}, function () {return 'N';} + */ + public static function unl($cx, $var, $in, $truecb, $falsecb = null) { + return self::ifv($cx, $var, $in, $falsecb, $truecb); + } + + /** + * LightnCandy runtime method for {{^var}} inverted section. + * + * @param array<string,array|string|integer> $cx render time context + * @param array<array|string|integer>|string|integer|null $v value to be tested + * + * @return boolean Return true when the value is not null nor false. + * + * @expect true when input array(), null + * @expect false when input array(), 0 + * @expect true when input array(), false + * @expect false when input array(), 'false' + * @expect true when input array(), array() + * @expect false when input array(), array('1') + */ + public static function isec($cx, $v) { + return is_null($v) || ($v === false) || (is_array($v) && (count($v) === 0)); + } + + /** + * LightnCandy runtime method for {{{var}}} . + * + * @param array<string,array|string|integer> $cx render time context + * @param array<array|string|integer>|string|integer|null $v value to be output + * + * @return string The raw value of the specified variable + * + * @expect true when input array('flags' => array('jstrue' => 0)), true + * @expect 'true' when input array('flags' => array('jstrue' => 1)), true + * @expect '' when input array('flags' => array('jstrue' => 0)), false + * @expect 'false' when input array('flags' => array('jstrue' => 1)), false + * @expect 'false' when input array('flags' => array('jstrue' => 1)), false, true + * @expect 'Array' when input array('flags' => array('jstrue' => 1, 'jsobj' => 0)), array('a', 'b') + * @expect 'a,b' when input array('flags' => array('jstrue' => 1, 'jsobj' => 1)), array('a', 'b') + * @expect '[object Object]' when input array('flags' => array('jstrue' => 1, 'jsobj' => 1)), array('a', 'c' => 'b') + * @expect '[object Object]' when input array('flags' => array('jstrue' => 1, 'jsobj' => 1)), array('c' => 'b') + * @expect 'a,true' when input array('flags' => array('jstrue' => 1, 'jsobj' => 1)), array('a', true) + * @expect 'a,1' when input array('flags' => array('jstrue' => 0, 'jsobj' => 1)), array('a',true) + * @expect 'a,' when input array('flags' => array('jstrue' => 0, 'jsobj' => 1)), array('a',false) + * @expect 'a,false' when input array('flags' => array('jstrue' => 1, 'jsobj' => 1)), array('a',false) + */ + public static function raw($cx, $v) { + if ($v === true) { + if ($cx['flags']['jstrue']) { + return 'true'; + } + } + + if (($v === false)) { + if ($cx['flags']['jstrue']) { + return 'false'; + } + } + + if (is_array($v)) { + if ($cx['flags']['jsobj']) { + if (count(array_diff_key($v, array_keys(array_keys($v)))) > 0) { + return '[object Object]'; + } else { + $ret = array(); + foreach ($v as $k => $vv) { + $ret[] = self::raw($cx, $vv); + } + return join(',', $ret); + } + } + } + + return "$v"; + } + + /** + * LightnCandy runtime method for {{var}} . + * + * @param array<string,array|string|integer> $cx render time context + * @param array<array|string|integer>|string|integer|null $var value to be htmlencoded + * + * @return string The htmlencoded value of the specified variable + * + * @expect 'a' when input array(), 'a' + * @expect 'a&b' when input array(), 'a&b' + * @expect 'a'b' when input array(), 'a\'b' + */ + public static function enc($cx, $var) { + return htmlentities(self::raw($cx, $var), ENT_QUOTES, 'UTF-8'); + } + + /** + * LightnCandy runtime method for {{var}} , and deal with single quote to same as handlebars.js . + * + * @param array<string,array|string|integer> $cx render time context + * @param array<array|string|integer>|string|integer|null $var value to be htmlencoded + * + * @return string The htmlencoded value of the specified variable + * + * @expect 'a' when input array(), 'a' + * @expect 'a&b' when input array(), 'a&b' + * @expect 'a'b' when input array(), 'a\'b' + * @expect '`a'b' when input array(), '`a\'b' + */ + public static function encq($cx, $var) { + return preg_replace('/`/', '`', preg_replace('/'/', ''', htmlentities(self::raw($cx, $var), ENT_QUOTES, 'UTF-8'))); + } + + /** + * LightnCandy runtime method for {{#var}} section. + * + * @param array<string,array|string|integer> $cx render time context + * @param array<array|string|integer>|string|integer|null $v value for the section + * @param array<array|string|integer>|string|integer|null $in input data with current scope + * @param boolean $each true when rendering #each + * @param Closure $cb callback function to render child context + * @param Closure|null $else callback function to render child context when {{else}} + * + * @return string The rendered string of the section + * + * @expect '' when input array('flags' => array('spvar' => 0)), false, false, false, function () {return 'A';} + * @expect '' when input array('flags' => array('spvar' => 0)), null, null, false, function () {return 'A';} + * @expect 'A' when input array('flags' => array('spvar' => 0)), true, true, false, function () {return 'A';} + * @expect 'A' when input array('flags' => array('spvar' => 0)), 0, 0, false, function () {return 'A';} + * @expect '-a=' when input array('flags' => array('spvar' => 0)), array('a'), array('a'), false, function ($c, $i) {return "-$i=";} + * @expect '-a=-b=' when input array('flags' => array('spvar' => 0)), array('a','b'), array('a','b'), false, function ($c, $i) {return "-$i=";} + * @expect '' when input array('flags' => array('spvar' => 0)), 'abc', 'abc', true, function ($c, $i) {return "-$i=";} + * @expect '-b=' when input array('flags' => array('spvar' => 0, 'mustsec' => 0)), array('a' => 'b'), array('a' => 'b'), true, function ($c, $i) {return "-$i=";} + * @expect '1' when input array('flags' => array('spvar' => 0)), 'b', 'b', false, function ($c, $i) {return count($i);} + * @expect '1' when input array('flags' => array('spvar' => 0)), 1, 1, false, function ($c, $i) {return print_r($i, true);} + * @expect '0' when input array('flags' => array('spvar' => 0)), 0, 0, false, function ($c, $i) {return print_r($i, true);} + * @expect '{"b":"c"}' when input array('flags' => array('spvar' => 0, 'mustsec' => 0)), array('b' => 'c'), array('b' => 'c'), false, function ($c, $i) {return json_encode($i);} + * @expect 'inv' when input array('flags' => array('spvar' => 0)), array(), 0, true, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + * @expect 'inv' when input array('flags' => array('spvar' => 0)), array(), 0, false, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + * @expect 'inv' when input array('flags' => array('spvar' => 0)), false, 0, true, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + * @expect 'inv' when input array('flags' => array('spvar' => 0)), false, 0, false, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + * @expect 'inv' when input array('flags' => array('spvar' => 0)), '', 0, true, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + * @expect 'cb' when input array('flags' => array('spvar' => 0)), '', 0, false, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + * @expect 'inv' when input array('flags' => array('spvar' => 0)), 0, 0, true, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + * @expect 'cb' when input array('flags' => array('spvar' => 0)), 0, 0, false, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + * @expect 'inv' when input array('flags' => array('spvar' => 0)), new stdClass, 0, true, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + * @expect 'cb' when input array('flags' => array('spvar' => 0)), new stdClass, 0, false, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + * @expect '268' when input array('flags' => array('spvar' => 1)), array(1,3,4), 0, false, function ($c, $i) {return $i * 2;} + * @expect '038' when input array('flags' => array('spvar' => 1), 'sp_vars'=>array()), array(1,3,'a'=>4), 0, true, function ($c, $i) {return $i * $c['sp_vars']['index'];} + */ + public static function sec($cx, $v, $in, $each, $cb, $else = null) { + $isAry = is_array($v); + $isTrav = $v instanceof Traversable; + $loop = $each; + $keys = null; + $last = null; + $isObj = false; + + if ($isAry && $else !== null && count($v) === 0) { + return $else($cx, $in); + } + + // #var, detect input type is object or not + if (!$loop && $isAry) { + $keys = array_keys($v); + $loop = (count(array_diff_key($v, array_keys($keys))) == 0); + $isObj = !$loop; + } + + if (($loop && $isAry) || $isTrav) { + if ($each && !$isTrav) { + // Detect input type is object or not when never done once + if ($keys == null) { + $keys = array_keys($v); + $isObj = (count(array_diff_key($v, array_keys($keys))) > 0); + } + } + $ret = array(); + $cx['scopes'][] = $in; + $i = 0; + if ($cx['flags']['spvar']) { + $old_spvar = $cx['sp_vars']; + $cx['sp_vars'] = array( + '_parent' => $old_spvar, + 'root' => $old_spvar['root'], + ); + if (!$isTrav) { + $last = count($keys) - 1; + } + } + foreach ($v as $index => $raw) { + if ($cx['flags']['spvar']) { + $cx['sp_vars']['first'] = ($i === 0); + $cx['sp_vars']['last'] = ($i == $last); + $cx['sp_vars']['key'] = $index; + $cx['sp_vars']['index'] = $i; + $i++; + } + $ret[] = $cb($cx, $raw); + } + if ($cx['flags']['spvar']) { + if ($isObj) { + unset($cx['sp_vars']['key']); + } else { + unset($cx['sp_vars']['last']); + } + unset($cx['sp_vars']['index']); + unset($cx['sp_vars']['first']); + $cx['sp_vars'] = $old_spvar; + } + array_pop($cx['scopes']); + return join('', $ret); + } + if ($each) { + if ($else !== null) { + $cx['scopes'][] = $in; + $ret = $else($cx, $v); + array_pop($cx['scopes']); + return $ret; + } + return ''; + } + if ($isAry) { + if ($cx['flags']['mustsec']) { + $cx['scopes'][] = $v; + } + $ret = $cb($cx, $v); + if ($cx['flags']['mustsec']) { + array_pop($cx['scopes']); + } + return $ret; + } + + if ($v === true) { + return $cb($cx, $in); + } + + if (!is_null($v) && ($v !== false)) { + return $cb($cx, $v); + } + + if ($else !== null) { + return $else($cx, $in); + } + + return ''; + } + + /** + * LightnCandy runtime method for {{#with var}} . + * + * @param array<string,array|string|integer> $cx render time context + * @param array<array|string|integer>|string|integer|null $v value to be the new context + * @param array<array|string|integer>|string|integer|null $in input data with current scope + * @param Closure $cb callback function to render child context + * @param Closure|null $else callback function to render child context when {{else}} + * + * @return string The rendered string of the token + * + * @expect '' when input array(), false, false, function () {return 'A';} + * @expect '' when input array(), null, null, function () {return 'A';} + * @expect '-Array=' when input array(), array('a'=>'b'), array('a' => 'b'), function ($c, $i) {return "-$i=";} + * @expect '-b=' when input array(), 'b', array('a' => 'b'), function ($c, $i) {return "-$i=";} + */ + public static function wi($cx, $v, $in, $cb, $else = null) { + if (($v === false) || ($v === null)) { + return $else ? $else($cx, $in) : ''; + } + $cx['scopes'][] = $in; + $ret = $cb($cx, $v); + array_pop($cx['scopes']); + return $ret; + } + + /** + * LightnCandy runtime method for {{> partial}} . + * + * @param array<string,array|string|integer> $cx render time context + * @param string $p partial name + * @param array<array|string|integer>|string|integer|null $v value to be the new context + * + * @return string The rendered string of the partial + * + */ + public static function p($cx, $p, $v, $sp = '') { + return call_user_func($cx['partials'][$p], $cx, is_array($v[0][0]) ? array_merge($v[0][0], $v[1]) : $v[0][0], $sp); + } + + /** + * LightnCandy runtime method for custom helpers. + * + * @param array<string,array|string|integer> $cx render time context + * @param string $ch the name of custom helper to be executed + * @param array<array> $vars variables for the helper + * @param string $op the name of variable resolver. should be one of: 'raw', 'enc', or 'encq'. + * + * @return string The rendered string of the token + * + * @expect '=-=' when input array('helpers' => array('a' => function ($i) {return "=$i[0]=";})), 'a', array(array('-'),array()), 'raw' + * @expect '=&=' when input array('helpers' => array('a' => function ($i) {return "=$i[0]=";})), 'a', array(array('&'),array()), 'enc' + * @expect '='=' when input array('helpers' => array('a' => function ($i) {return "=$i[0]=";})), 'a', array(array('\''),array()), 'encq' + * @expect '=b=' when input array('helpers' => array('a' => function ($i,$j) {return "={$j['a']}=";})), 'a', array(array(),array('a' => 'b')), 'raw' + */ + public static function ch($cx, $ch, $vars, $op) { + return self::chret(call_user_func_array($cx['helpers'][$ch], $vars), $op); + } + + /** + * LightnCandy runtime method to handle response of custom helpers. + * + * @param string|array<string,array|string|integer> $ret return value from custom helper + * @param string $op the name of variable resolver. should be one of: 'raw', 'enc', or 'encq'. + * + * @return string The rendered string of the token + * + * @expect '=&=' when input '=&=', 'raw' + * @expect '=&'=' when input '=&\'=', 'enc' + * @expect '=&'=' when input '=&\'=', 'encq' + * @expect '=&'=' when input array('=&\'='), 'enc' + * @expect '=&'=' when input array('=&\'='), 'encq' + * @expect '=&=' when input array('=&=', false), 'enc' + * @expect '=&=' when input array('=&=', false), 'raw' + * @expect '=&=' when input array('=&=', 'raw'), 'enc' + * @expect '=&'=' when input array('=&\'=', 'encq'), 'raw' + */ + public static function chret($ret, $op) { + if (is_array($ret)) { + if (isset($ret[1]) && $ret[1]) { + $op = $ret[1]; + } + $ret = $ret[0]; + } + + switch ($op) { + case 'enc': + return htmlentities($ret, ENT_QUOTES, 'UTF-8'); + case 'encq': + return preg_replace('/'/', ''', htmlentities($ret, ENT_QUOTES, 'UTF-8')); + } + return $ret; + } + + /** + * LightnCandy runtime method for Handlebars.js style custom helpers. + * + * @param array<string,array|string|integer> $cx render time context + * @param string $ch the name of custom helper to be executed + * @param array<array|string|integer>|string|integer|null $vars variables for the helper + * @param string $op the name of variable resolver. should be one of: 'raw', 'enc', or 'encq'. + * @param boolean $inverted the logic will be inverted + * @param Closure $cb callback function to render child context + * @param Closure|null $else callback function to render child context when {{else}} + * + * @return string The rendered string of the token + */ + public static function hbch($cx, $ch, $vars, $op, $inverted, $cb = false, $else = false) { + $isBlock = (is_object($cb) && ($cb instanceof Closure)); + $args = $vars[0]; + $options = array( + 'name' => $ch, + 'hash' => $vars[1] + ); + + // $invert the logic + if ($inverted) { + $tmp = $else; + $else = $cb; + $cb = $tmp; + } + + if ($isBlock) { + $options['fn'] = function ($context = '_NO_INPUT_HERE_') use ($cx, $op, $cb) { + if ($cx['flags']['echo']) { + ob_start(); + } + if ($context === '_NO_INPUT_HERE_') { + $ret = $cb($cx, $op); + } else { + $cx['scopes'][] = $op; + $ret = $cb($cx, $context); + array_pop($cx['scopes']); + } + return $cx['flags']['echo'] ? ob_get_clean() : $ret; + }; + } + + if ($else) { + $options['inverse'] = function ($context = '_NO_INPUT_HERE_') use ($cx, $op, $else) { + if ($cx['flags']['echo']) { + ob_start(); + } + if ($context === '_NO_INPUT_HERE_') { + $ret = $else($cx, $op); + } else { + $cx['scopes'][] = $op; + $ret = $else($cx, $context); + array_pop($cx['scopes']); + } + return $cx['flags']['echo'] ? ob_get_clean() : $ret; + }; + } + + // prepare $options['data'] + if ($cx['flags']['spvar']) { + $options['data'] = $cx['sp_vars']; + $options['data']['root'] = $cx['scopes'][0]; + } + + $args[] = $options; + $e = null; + $r = true; + + try { + $r = call_user_func_array($cx['hbhelpers'][$ch], $args); + } catch (Exception $E) { + $e = "LCRun3: call custom helper '$ch' error: " . $E->getMessage(); + } + + if ($r === false) { + if ($e === null) { + $e = "LCRun3: call custom helper '$ch' error"; + } + } + + if($e !== null) { + if ($cx['flags']['debug'] & self::DEBUG_ERROR_LOG) { + error_log($e); + } + if ($cx['flags']['debug'] & self::DEBUG_ERROR_EXCEPTION) { + throw new Exception($e); + } + } + + return self::chret($r, $isBlock ? 'raw' : $op); + } + + /** + * LightnCandy runtime method for block custom helpers. + * + * @param array<string,array|string|integer> $cx render time context + * @param string $ch the name of custom helper to be executed + * @param array<array|string|integer>|string|integer|null $vars variables for the helper + * @param array<array|string|integer>|string|integer|null $in input data with current scope + * @param boolean $inverted the logic will be inverted + * @param Closure $cb callback function to render child context + * @param Closure|null $else callback function to render child context when {{else}} + * + * @return string The rendered string of the token + * + * @expect '4.2.3' when input array('blockhelpers' => array('a' => function ($cx) {return array($cx,2,3);})), 'a', array(0, 0), 4, false, function($cx, $i) {return implode('.', $i);} + * @expect '2.6.5' when input array('blockhelpers' => array('a' => function ($cx,$in) {return array($cx,$in[0],5);})), 'a', array('6', 0), 2, false, function($cx, $i) {return implode('.', $i);} + * @expect '' when input array('blockhelpers' => array('a' => function ($cx,$in) {})), 'a', array('6', 0), 2, false, function($cx, $i) {return implode('.', $i);} + */ + public static function bch($cx, $ch, $vars, $in, $inverted, $cb, $else = false) { + $r = call_user_func($cx['blockhelpers'][$ch], $in, $vars[0], $vars[1]); + + // $invert the logic + if ($inverted) { + $tmp = $else; + $else = $cb; + $cb = $tmp; + } + + $ret = ''; + if (is_null($r)) { + if ($else) { + $cx['scopes'][] = $in; + $ret = $else($cx, $r); + array_pop($cx['scopes']); + } + } else { + if ($cb) { + $cx['scopes'][] = $in; + $ret = $cb($cx, $r); + array_pop($cx['scopes']); + } + } + + return $ret; + } +} + +?>
\ No newline at end of file diff --git a/vendor/zordius/lightncandy/tests/LCRun3Test.php b/vendor/zordius/lightncandy/tests/LCRun3Test.php new file mode 100644 index 00000000..63d6348e --- /dev/null +++ b/vendor/zordius/lightncandy/tests/LCRun3Test.php @@ -0,0 +1,376 @@ +<?php +/** + * Generated by build/gen_test + */ +require_once('src/lightncandy.php'); + +class LCRun3Test extends PHPUnit_Framework_TestCase +{ + /** + * @covers LCRun3::debug + */ + public function testOn_debug() { + $method = new ReflectionMethod('LCRun3', 'debug'); + $this->assertEquals('{{123}}', $method->invoke(null, + '123', 'miss', array('flags' => array('debug' => LCRun3::DEBUG_TAGS)), '' + )); + $this->assertEquals('<!--MISSED((-->{{#123}}<!--))--><!--SKIPPED--><!--MISSED((-->{{/123}}<!--))-->', $method->invoke(null, + '123', 'wi', array('flags' => array('debug' => LCRun3::DEBUG_TAGS_HTML)), false, false, function () {return 'A';} + )); + } + /** + * @covers LCRun3::v + */ + public function testOn_v() { + $method = new ReflectionMethod('LCRun3', 'v'); + $this->assertEquals(null, $method->invoke(null, + array('scopes' => array(), 'flags' => array('prop' => 0, 'method' => 0, 'mustlok' => 0)), 0, array('a', 'b') + )); + $this->assertEquals(3, $method->invoke(null, + array('scopes' => array(), 'flags' => array('prop' => 0, 'method' => 0), 'mustlok' => 0), array('a' => array('b' => 3)), array('a', 'b') + )); + $this->assertEquals(null, $method->invoke(null, + array('scopes' => array(), 'flags' => array('prop' => 0, 'method' => 0, 'mustlok' => 0)), (Object) array('a' => array('b' => 3)), array('a', 'b') + )); + $this->assertEquals(3, $method->invoke(null, + array('scopes' => array(), 'flags' => array('prop' => 1, 'method' => 0, 'mustlok' => 0)), (Object) array('a' => array('b' => 3)), array('a', 'b') + )); + } + /** + * @covers LCRun3::ifvar + */ + public function testOn_ifvar() { + $method = new ReflectionMethod('LCRun3', 'ifvar'); + $this->assertEquals(false, $method->invoke(null, + array(), null + )); + $this->assertEquals(false, $method->invoke(null, + array(), 0 + )); + $this->assertEquals(false, $method->invoke(null, + array(), false + )); + $this->assertEquals(true, $method->invoke(null, + array(), true + )); + $this->assertEquals(true, $method->invoke(null, + array(), 1 + )); + $this->assertEquals(false, $method->invoke(null, + array(), '' + )); + $this->assertEquals(false, $method->invoke(null, + array(), array() + )); + $this->assertEquals(true, $method->invoke(null, + array(), array('') + )); + $this->assertEquals(true, $method->invoke(null, + array(), array(0) + )); + } + /** + * @covers LCRun3::ifv + */ + public function testOn_ifv() { + $method = new ReflectionMethod('LCRun3', 'ifv'); + $this->assertEquals('', $method->invoke(null, + array('scopes' => array()), null, array(), null + )); + $this->assertEquals('', $method->invoke(null, + array('scopes' => array()), null, array(), function () {return 'Y';} + )); + $this->assertEquals('Y', $method->invoke(null, + array('scopes' => array()), 1, array(), function () {return 'Y';} + )); + $this->assertEquals('N', $method->invoke(null, + array('scopes' => array()), null, array(), function () {return 'Y';}, function () {return 'N';} + )); + } + /** + * @covers LCRun3::unl + */ + public function testOn_unl() { + $method = new ReflectionMethod('LCRun3', 'unl'); + $this->assertEquals('', $method->invoke(null, + array('scopes' => array()), null, array(), null + )); + $this->assertEquals('Y', $method->invoke(null, + array('scopes' => array()), null, array(), function () {return 'Y';} + )); + $this->assertEquals('', $method->invoke(null, + array('scopes' => array()), 1, array(), function () {return 'Y';} + )); + $this->assertEquals('Y', $method->invoke(null, + array('scopes' => array()), null, array(), function () {return 'Y';}, function () {return 'N';} + )); + $this->assertEquals('N', $method->invoke(null, + array('scopes' => array()), true, array(), function () {return 'Y';}, function () {return 'N';} + )); + } + /** + * @covers LCRun3::isec + */ + public function testOn_isec() { + $method = new ReflectionMethod('LCRun3', 'isec'); + $this->assertEquals(true, $method->invoke(null, + array(), null + )); + $this->assertEquals(false, $method->invoke(null, + array(), 0 + )); + $this->assertEquals(true, $method->invoke(null, + array(), false + )); + $this->assertEquals(false, $method->invoke(null, + array(), 'false' + )); + $this->assertEquals(true, $method->invoke(null, + array(), array() + )); + $this->assertEquals(false, $method->invoke(null, + array(), array('1') + )); + } + /** + * @covers LCRun3::raw + */ + public function testOn_raw() { + $method = new ReflectionMethod('LCRun3', 'raw'); + $this->assertEquals(true, $method->invoke(null, + array('flags' => array('jstrue' => 0)), true + )); + $this->assertEquals('true', $method->invoke(null, + array('flags' => array('jstrue' => 1)), true + )); + $this->assertEquals('', $method->invoke(null, + array('flags' => array('jstrue' => 0)), false + )); + $this->assertEquals('false', $method->invoke(null, + array('flags' => array('jstrue' => 1)), false + )); + $this->assertEquals('false', $method->invoke(null, + array('flags' => array('jstrue' => 1)), false, true + )); + $this->assertEquals('Array', $method->invoke(null, + array('flags' => array('jstrue' => 1, 'jsobj' => 0)), array('a', 'b') + )); + $this->assertEquals('a,b', $method->invoke(null, + array('flags' => array('jstrue' => 1, 'jsobj' => 1)), array('a', 'b') + )); + $this->assertEquals('[object Object]', $method->invoke(null, + array('flags' => array('jstrue' => 1, 'jsobj' => 1)), array('a', 'c' => 'b') + )); + $this->assertEquals('[object Object]', $method->invoke(null, + array('flags' => array('jstrue' => 1, 'jsobj' => 1)), array('c' => 'b') + )); + $this->assertEquals('a,true', $method->invoke(null, + array('flags' => array('jstrue' => 1, 'jsobj' => 1)), array('a', true) + )); + $this->assertEquals('a,1', $method->invoke(null, + array('flags' => array('jstrue' => 0, 'jsobj' => 1)), array('a',true) + )); + $this->assertEquals('a,', $method->invoke(null, + array('flags' => array('jstrue' => 0, 'jsobj' => 1)), array('a',false) + )); + $this->assertEquals('a,false', $method->invoke(null, + array('flags' => array('jstrue' => 1, 'jsobj' => 1)), array('a',false) + )); + } + /** + * @covers LCRun3::enc + */ + public function testOn_enc() { + $method = new ReflectionMethod('LCRun3', 'enc'); + $this->assertEquals('a', $method->invoke(null, + array(), 'a' + )); + $this->assertEquals('a&b', $method->invoke(null, + array(), 'a&b' + )); + $this->assertEquals('a'b', $method->invoke(null, + array(), 'a\'b' + )); + } + /** + * @covers LCRun3::encq + */ + public function testOn_encq() { + $method = new ReflectionMethod('LCRun3', 'encq'); + $this->assertEquals('a', $method->invoke(null, + array(), 'a' + )); + $this->assertEquals('a&b', $method->invoke(null, + array(), 'a&b' + )); + $this->assertEquals('a'b', $method->invoke(null, + array(), 'a\'b' + )); + $this->assertEquals('`a'b', $method->invoke(null, + array(), '`a\'b' + )); + } + /** + * @covers LCRun3::sec + */ + public function testOn_sec() { + $method = new ReflectionMethod('LCRun3', 'sec'); + $this->assertEquals('', $method->invoke(null, + array('flags' => array('spvar' => 0)), false, false, false, function () {return 'A';} + )); + $this->assertEquals('', $method->invoke(null, + array('flags' => array('spvar' => 0)), null, null, false, function () {return 'A';} + )); + $this->assertEquals('A', $method->invoke(null, + array('flags' => array('spvar' => 0)), true, true, false, function () {return 'A';} + )); + $this->assertEquals('A', $method->invoke(null, + array('flags' => array('spvar' => 0)), 0, 0, false, function () {return 'A';} + )); + $this->assertEquals('-a=', $method->invoke(null, + array('flags' => array('spvar' => 0)), array('a'), array('a'), false, function ($c, $i) {return "-$i=";} + )); + $this->assertEquals('-a=-b=', $method->invoke(null, + array('flags' => array('spvar' => 0)), array('a','b'), array('a','b'), false, function ($c, $i) {return "-$i=";} + )); + $this->assertEquals('', $method->invoke(null, + array('flags' => array('spvar' => 0)), 'abc', 'abc', true, function ($c, $i) {return "-$i=";} + )); + $this->assertEquals('-b=', $method->invoke(null, + array('flags' => array('spvar' => 0, 'mustsec' => 0)), array('a' => 'b'), array('a' => 'b'), true, function ($c, $i) {return "-$i=";} + )); + $this->assertEquals('1', $method->invoke(null, + array('flags' => array('spvar' => 0)), 'b', 'b', false, function ($c, $i) {return count($i);} + )); + $this->assertEquals('1', $method->invoke(null, + array('flags' => array('spvar' => 0)), 1, 1, false, function ($c, $i) {return print_r($i, true);} + )); + $this->assertEquals('0', $method->invoke(null, + array('flags' => array('spvar' => 0)), 0, 0, false, function ($c, $i) {return print_r($i, true);} + )); + $this->assertEquals('{"b":"c"}', $method->invoke(null, + array('flags' => array('spvar' => 0, 'mustsec' => 0)), array('b' => 'c'), array('b' => 'c'), false, function ($c, $i) {return json_encode($i);} + )); + $this->assertEquals('inv', $method->invoke(null, + array('flags' => array('spvar' => 0)), array(), 0, true, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + )); + $this->assertEquals('inv', $method->invoke(null, + array('flags' => array('spvar' => 0)), array(), 0, false, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + )); + $this->assertEquals('inv', $method->invoke(null, + array('flags' => array('spvar' => 0)), false, 0, true, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + )); + $this->assertEquals('inv', $method->invoke(null, + array('flags' => array('spvar' => 0)), false, 0, false, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + )); + $this->assertEquals('inv', $method->invoke(null, + array('flags' => array('spvar' => 0)), '', 0, true, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + )); + $this->assertEquals('cb', $method->invoke(null, + array('flags' => array('spvar' => 0)), '', 0, false, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + )); + $this->assertEquals('inv', $method->invoke(null, + array('flags' => array('spvar' => 0)), 0, 0, true, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + )); + $this->assertEquals('cb', $method->invoke(null, + array('flags' => array('spvar' => 0)), 0, 0, false, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + )); + $this->assertEquals('inv', $method->invoke(null, + array('flags' => array('spvar' => 0)), new stdClass, 0, true, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + )); + $this->assertEquals('cb', $method->invoke(null, + array('flags' => array('spvar' => 0)), new stdClass, 0, false, function ($c, $i) {return 'cb';}, function ($c, $i) {return 'inv';} + )); + $this->assertEquals('268', $method->invoke(null, + array('flags' => array('spvar' => 1)), array(1,3,4), 0, false, function ($c, $i) {return $i * 2;} + )); + $this->assertEquals('038', $method->invoke(null, + array('flags' => array('spvar' => 1), 'sp_vars'=>array()), array(1,3,'a'=>4), 0, true, function ($c, $i) {return $i * $c['sp_vars']['index'];} + )); + } + /** + * @covers LCRun3::wi + */ + public function testOn_wi() { + $method = new ReflectionMethod('LCRun3', 'wi'); + $this->assertEquals('', $method->invoke(null, + array(), false, false, function () {return 'A';} + )); + $this->assertEquals('', $method->invoke(null, + array(), null, null, function () {return 'A';} + )); + $this->assertEquals('-Array=', $method->invoke(null, + array(), array('a'=>'b'), array('a' => 'b'), function ($c, $i) {return "-$i=";} + )); + $this->assertEquals('-b=', $method->invoke(null, + array(), 'b', array('a' => 'b'), function ($c, $i) {return "-$i=";} + )); + } + /** + * @covers LCRun3::ch + */ + public function testOn_ch() { + $method = new ReflectionMethod('LCRun3', 'ch'); + $this->assertEquals('=-=', $method->invoke(null, + array('helpers' => array('a' => function ($i) {return "=$i[0]=";})), 'a', array(array('-'),array()), 'raw' + )); + $this->assertEquals('=&=', $method->invoke(null, + array('helpers' => array('a' => function ($i) {return "=$i[0]=";})), 'a', array(array('&'),array()), 'enc' + )); + $this->assertEquals('='=', $method->invoke(null, + array('helpers' => array('a' => function ($i) {return "=$i[0]=";})), 'a', array(array('\''),array()), 'encq' + )); + $this->assertEquals('=b=', $method->invoke(null, + array('helpers' => array('a' => function ($i,$j) {return "={$j['a']}=";})), 'a', array(array(),array('a' => 'b')), 'raw' + )); + } + /** + * @covers LCRun3::chret + */ + public function testOn_chret() { + $method = new ReflectionMethod('LCRun3', 'chret'); + $this->assertEquals('=&=', $method->invoke(null, + '=&=', 'raw' + )); + $this->assertEquals('=&'=', $method->invoke(null, + '=&\'=', 'enc' + )); + $this->assertEquals('=&'=', $method->invoke(null, + '=&\'=', 'encq' + )); + $this->assertEquals('=&'=', $method->invoke(null, + array('=&\'='), 'enc' + )); + $this->assertEquals('=&'=', $method->invoke(null, + array('=&\'='), 'encq' + )); + $this->assertEquals('=&=', $method->invoke(null, + array('=&=', false), 'enc' + )); + $this->assertEquals('=&=', $method->invoke(null, + array('=&=', false), 'raw' + )); + $this->assertEquals('=&=', $method->invoke(null, + array('=&=', 'raw'), 'enc' + )); + $this->assertEquals('=&'=', $method->invoke(null, + array('=&\'=', 'encq'), 'raw' + )); + } + /** + * @covers LCRun3::bch + */ + public function testOn_bch() { + $method = new ReflectionMethod('LCRun3', 'bch'); + $this->assertEquals('4.2.3', $method->invoke(null, + array('blockhelpers' => array('a' => function ($cx) {return array($cx,2,3);})), 'a', array(0, 0), 4, false, function($cx, $i) {return implode('.', $i);} + )); + $this->assertEquals('2.6.5', $method->invoke(null, + array('blockhelpers' => array('a' => function ($cx,$in) {return array($cx,$in[0],5);})), 'a', array('6', 0), 2, false, function($cx, $i) {return implode('.', $i);} + )); + $this->assertEquals('', $method->invoke(null, + array('blockhelpers' => array('a' => function ($cx,$in) {})), 'a', array('6', 0), 2, false, function($cx, $i) {return implode('.', $i);} + )); + } +} +?>
\ No newline at end of file diff --git a/vendor/zordius/lightncandy/tests/LightnCandyTest.php b/vendor/zordius/lightncandy/tests/LightnCandyTest.php new file mode 100644 index 00000000..6f82f2bd --- /dev/null +++ b/vendor/zordius/lightncandy/tests/LightnCandyTest.php @@ -0,0 +1,460 @@ +<?php +/** + * Generated by build/gen_test + */ +require_once('src/lightncandy.php'); + +class LightnCandyTest extends PHPUnit_Framework_TestCase +{ + /** + * @covers LightnCandy::buildHelperTable + */ + public function testOn_buildHelperTable() { + $method = new ReflectionMethod('LightnCandy', 'buildHelperTable'); + $method->setAccessible(true); + $this->assertEquals(array(), $method->invoke(null, + array(), array() + )); + $this->assertEquals(array('flags' => array('exhlp' => 1)), $method->invoke(null, + array('flags' => array('exhlp' => 1)), array('helpers' => array('abc')) + )); + $this->assertEquals(array('error' => array('Can not find custom helper function defination abc() !'), 'flags' => array('exhlp' => 0)), $method->invoke(null, + array('error' => array(), 'flags' => array('exhlp' => 0)), array('helpers' => array('abc')) + )); + $this->assertEquals(array('flags' => array('exhlp' => 1), 'helpers' => array('LCRun3::raw' => 'LCRun3::raw')), $method->invoke(null, + array('flags' => array('exhlp' => 1), 'helpers' => array()), array('helpers' => array('LCRun3::raw')) + )); + $this->assertEquals(array('flags' => array('exhlp' => 1), 'helpers' => array('test' => 'LCRun3::raw')), $method->invoke(null, + array('flags' => array('exhlp' => 1), 'helpers' => array()), array('helpers' => array('test' => 'LCRun3::raw')) + )); + } + /** + * @covers LightnCandy::buildCXFileext + */ + public function testOn_buildCXFileext() { + $method = new ReflectionMethod('LightnCandy', 'buildCXFileext'); + $method->setAccessible(true); + $this->assertEquals(array('.tmpl'), $method->invoke(null, + array() + )); + $this->assertEquals(array('test'), $method->invoke(null, + array('fileext' => 'test') + )); + $this->assertEquals(array('test1'), $method->invoke(null, + array('fileext' => array('test1')) + )); + $this->assertEquals(array('test2', 'test3'), $method->invoke(null, + array('fileext' => array('test2', 'test3')) + )); + } + /** + * @covers LightnCandy::buildCXBasedir + */ + public function testOn_buildCXBasedir() { + $method = new ReflectionMethod('LightnCandy', 'buildCXBasedir'); + $method->setAccessible(true); + $this->assertEquals(array(), $method->invoke(null, + array() + )); + $this->assertEquals(array(), $method->invoke(null, + array('basedir' => array()) + )); + $this->assertEquals(array('src'), $method->invoke(null, + array('basedir' => array('src')) + )); + $this->assertEquals(array('src'), $method->invoke(null, + array('basedir' => array('src', 'dir_not_found')) + )); + $this->assertEquals(array('src', 'tests'), $method->invoke(null, + array('basedir' => array('src', 'tests')) + )); + } + /** + * @covers LightnCandy::getPHPCode + */ + public function testOn_getPHPCode() { + $method = new ReflectionMethod('LightnCandy', 'getPHPCode'); + $method->setAccessible(true); + $this->assertEquals('function($a) {return;}', $method->invoke(null, + function ($a) {return;} + )); + $this->assertEquals('function($a) {return;}', $method->invoke(null, + function ($a) {return;} + )); + } + /** + * @covers LightnCandy::handleError + */ + public function testOn_handleError() { + $method = new ReflectionMethod('LightnCandy', 'handleError'); + $method->setAccessible(true); + $this->assertEquals(true, $method->invoke(null, + array('level' => 1, 'stack' => array('X'), 'flags' => array('errorlog' => 0, 'exception' => 0), 'error' => array()) + )); + $this->assertEquals(false, $method->invoke(null, + array('level' => 0, 'error' => array()) + )); + $this->assertEquals(true, $method->invoke(null, + array('level' => 0, 'error' => array('some error'), 'flags' => array('errorlog' => 0, 'exception' => 0)) + )); + } + /** + * @covers LightnCandy::getBoolStr + */ + public function testOn_getBoolStr() { + $method = new ReflectionMethod('LightnCandy', 'getBoolStr'); + $method->setAccessible(true); + $this->assertEquals('true', $method->invoke(null, + 1 + )); + $this->assertEquals('true', $method->invoke(null, + 999 + )); + $this->assertEquals('false', $method->invoke(null, + 0 + )); + $this->assertEquals('false', $method->invoke(null, + -1 + )); + } + /** + * @covers LightnCandy::getFuncName + */ + public function testOn_getFuncName() { + $method = new ReflectionMethod('LightnCandy', 'getFuncName'); + $method->setAccessible(true); + $this->assertEquals('LCRun3::test(', $method->invoke(null, + array('flags' => array('standalone' => 0, 'debug' => 0)), 'test', '' + )); + $this->assertEquals('LCRun3::test2(', $method->invoke(null, + array('flags' => array('standalone' => 0, 'debug' => 0)), 'test2', '' + )); + $this->assertEquals("\$cx['funcs']['test3'](", $method->invoke(null, + array('flags' => array('standalone' => 1, 'debug' => 0)), 'test3', '' + )); + $this->assertEquals('LCRun3::debug(\'abc\', \'test\', ', $method->invoke(null, + array('flags' => array('standalone' => 0, 'debug' => 1)), 'test', 'abc' + )); + } + /** + * @covers LightnCandy::getArrayStr + */ + public function testOn_getArrayStr() { + $method = new ReflectionMethod('LightnCandy', 'getArrayStr'); + $method->setAccessible(true); + $this->assertEquals('', $method->invoke(null, + array() + )); + $this->assertEquals('[a]', $method->invoke(null, + array('a') + )); + $this->assertEquals('[a][b][c]', $method->invoke(null, + array('a', 'b', 'c') + )); + } + /** + * @covers LightnCandy::getArrayCode + */ + public function testOn_getArrayCode() { + $method = new ReflectionMethod('LightnCandy', 'getArrayCode'); + $method->setAccessible(true); + $this->assertEquals('', $method->invoke(null, + array() + )); + $this->assertEquals("['a']", $method->invoke(null, + array('a') + )); + $this->assertEquals("['a']['b']['c']", $method->invoke(null, + array('a', 'b', 'c') + )); + } + /** + * @covers LightnCandy::getVariableNames + */ + public function testOn_getVariableNames() { + $method = new ReflectionMethod('LightnCandy', 'getVariableNames'); + $method->setAccessible(true); + $this->assertEquals(array('array(array($in),array())', array('this')), $method->invoke(null, + array(null), array('flags'=>array('spvar'=>true)) + )); + $this->assertEquals(array('array(array($in,$in),array())', array('this', 'this')), $method->invoke(null, + array(null, null), array('flags'=>array('spvar'=>true)) + )); + $this->assertEquals(array('array(array(),array(\'a\'=>$in))', array('this')), $method->invoke(null, + array('a' => null), array('flags'=>array('spvar'=>true)) + )); + } + /** + * @covers LightnCandy::getVariableName + */ + public function testOn_getVariableName() { + $method = new ReflectionMethod('LightnCandy', 'getVariableName'); + $method->setAccessible(true); + $this->assertEquals(array('$in', 'this'), $method->invoke(null, + array(null), array('flags'=>array('spvar'=>true,'debug'=>0)) + )); + $this->assertEquals(array('true', 'true'), $method->invoke(null, + array('true'), array('flags'=>array('spvar'=>true,'debug'=>0)), true + )); + $this->assertEquals(array('false', 'false'), $method->invoke(null, + array('false'), array('flags'=>array('spvar'=>true,'debug'=>0)), true + )); + $this->assertEquals(array(2, '2'), $method->invoke(null, + array('2'), array('flags'=>array('spvar'=>true,'debug'=>0)), true + )); + $this->assertEquals(array('((isset($in[\'@index\']) && is_array($in)) ? $in[\'@index\'] : null)', '[@index]'), $method->invoke(null, + array('@index'), array('flags'=>array('spvar'=>false,'debug'=>0,'prop'=>0,'method'=>0,'mustlok'=>0)) + )); + $this->assertEquals(array("((isset(\$cx['sp_vars']['index']) && is_array(\$cx['sp_vars'])) ? \$cx['sp_vars']['index'] : null)", '@[index]'), $method->invoke(null, + array('@index'), array('flags'=>array('spvar'=>true,'debug'=>0)) + )); + $this->assertEquals(array("((isset(\$cx['sp_vars']['key']) && is_array(\$cx['sp_vars'])) ? \$cx['sp_vars']['key'] : null)", '@[key]'), $method->invoke(null, + array('@key'), array('flags'=>array('spvar'=>true,'debug'=>0)) + )); + $this->assertEquals(array("((isset(\$cx['sp_vars']['first']) && is_array(\$cx['sp_vars'])) ? \$cx['sp_vars']['first'] : null)", '@[first]'), $method->invoke(null, + array('@first'), array('flags'=>array('spvar'=>true,'debug'=>0)) + )); + $this->assertEquals(array("((isset(\$cx['sp_vars']['last']) && is_array(\$cx['sp_vars'])) ? \$cx['sp_vars']['last'] : null)", '@[last]'), $method->invoke(null, + array('@last'), array('flags'=>array('spvar'=>true,'debug'=>0)) + )); + $this->assertEquals(array('\'a\'', '"a"'), $method->invoke(null, + array('"a"'), array('flags'=>array('spvar'=>true,'debug'=>0)) + )); + $this->assertEquals(array('((isset($in[\'a\']) && is_array($in)) ? $in[\'a\'] : null)', '[a]'), $method->invoke(null, + array('a'), array('flags'=>array('spvar'=>true,'debug'=>0,'prop'=>0,'method'=>0,'mustlok'=>0)) + )); + $this->assertEquals(array('((isset($cx[\'scopes\'][count($cx[\'scopes\'])-1][\'a\']) && is_array($cx[\'scopes\'][count($cx[\'scopes\'])-1])) ? $cx[\'scopes\'][count($cx[\'scopes\'])-1][\'a\'] : null)', '../[a]'), $method->invoke(null, + array(1,'a'), array('flags'=>array('spvar'=>true,'debug'=>0,'prop'=>0,'method'=>0,'mustlok'=>0)) + )); + $this->assertEquals(array('((isset($cx[\'scopes\'][count($cx[\'scopes\'])-3][\'a\']) && is_array($cx[\'scopes\'][count($cx[\'scopes\'])-3])) ? $cx[\'scopes\'][count($cx[\'scopes\'])-3][\'a\'] : null)', '../../../[a]'), $method->invoke(null, + array(3,'a'), array('flags'=>array('spvar'=>true,'debug'=>0,'prop'=>0,'method'=>0,'mustlok'=>0)) + )); + $this->assertEquals(array('((isset($in[\'id\']) && is_array($in)) ? $in[\'id\'] : null)', 'this.[id]'), $method->invoke(null, + array(null, 'id'), array('flags'=>array('spvar'=>true,'debug'=>0,'prop'=>0,'method'=>0,'mustlok'=>0)) + )); + $this->assertEquals(array('LCRun3::v($cx, $in, array(\'id\'))', 'this.[id]'), $method->invoke(null, + array(null, 'id'), array('flags'=>array('prop'=>true,'spvar'=>true,'debug'=>0,'method'=>0,'mustlok'=>0,'standalone'=>0)) + )); + } + /** + * @covers LightnCandy::getExpression + */ + public function testOn_getExpression() { + $method = new ReflectionMethod('LightnCandy', 'getExpression'); + $method->setAccessible(true); + $this->assertEquals('[a].[b]', $method->invoke(null, + 0, false, array('a', 'b') + )); + $this->assertEquals('@[root]', $method->invoke(null, + 0, true, array('root') + )); + $this->assertEquals('this', $method->invoke(null, + 0, false, null + )); + $this->assertEquals('this.[id]', $method->invoke(null, + 0, false, array(null, 'id') + )); + $this->assertEquals('@[root].[a].[b]', $method->invoke(null, + 0, true, array('root', 'a', 'b') + )); + $this->assertEquals('../../[a].[b]', $method->invoke(null, + 2, false, array('a', 'b') + )); + $this->assertEquals('../[a\'b]', $method->invoke(null, + 1, false, array('a\'b') + )); + } + /** + * @covers LightnCandy::fixVariable + */ + public function testOn_fixVariable() { + $method = new ReflectionMethod('LightnCandy', 'fixVariable'); + $method->setAccessible(true); + $this->assertEquals(array('this'), $method->invoke(null, + 'this', array('flags' => array('advar' => 0, 'this' => 0)) + )); + $this->assertEquals(array(null), $method->invoke(null, + 'this', array('flags' => array('advar' => 0, 'this' => 1)) + )); + $this->assertEquals(array(1, null), $method->invoke(null, + '../', array('flags' => array('advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + )); + $this->assertEquals(array(1, null), $method->invoke(null, + '../.', array('flags' => array('advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + )); + $this->assertEquals(array(1, null), $method->invoke(null, + '../this', array('flags' => array('advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + )); + $this->assertEquals(array(1, 'a'), $method->invoke(null, + '../a', array('flags' => array('advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + )); + $this->assertEquals(array(2, 'a', 'b'), $method->invoke(null, + '../../a.b', array('flags' => array('advar' => 0, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + )); + $this->assertEquals(array(2, '[a]', 'b'), $method->invoke(null, + '../../[a].b', array('flags' => array('advar' => 0, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + )); + $this->assertEquals(array(2, 'a', 'b'), $method->invoke(null, + '../../[a].b', array('flags' => array('advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + )); + $this->assertEquals(array('"a.b"'), $method->invoke(null, + '"a.b"', array('flags' => array('advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + )); + $this->assertEquals(array(null, 'id'), $method->invoke(null, + 'this.id', array('flags' => array('advar' => 1, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + )); + } + /** + * @covers LightnCandy::parseTokenArgs + */ + public function testOn_parseTokenArgs() { + $method = new ReflectionMethod('LightnCandy', 'parseTokenArgs'); + $method->setAccessible(true); + $this->assertEquals(array(false, array(array(null))), $method->invoke(null, + array(0,0,0,0,0,0,''), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + )); + $this->assertEquals(array(true, array(array(null))), $method->invoke(null, + array(0,0,0,'{{{',0,0,''), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + )); + $this->assertEquals(array(true, array(array(null))), $method->invoke(null, + array(0,0,0,0,0,0,''), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 1)) + )); + $this->assertEquals(array(false, array(array('a'))), $method->invoke(null, + array(0,0,0,0,0,0,'a'), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + )); + $this->assertEquals(array(false, array(array('a'), array('b'))), $method->invoke(null, + array(0,0,0,0,0,0,'a b'), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + )); + $this->assertEquals(array(false, array(array('a'), array('"b'), array('c"'))), $method->invoke(null, + array(0,0,0,0,0,0,'a "b c"'), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + )); + $this->assertEquals(array(false, array(array('a'), array('"b c"'))), $method->invoke(null, + array(0,0,0,0,0,0,'a "b c"'), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + )); + $this->assertEquals(array(false, array(array('a'), array('[b'), array('c]'))), $method->invoke(null, + array(0,0,0,0,0,0,'a [b c]'), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + )); + $this->assertEquals(array(false, array(array('a'), array('[b'), array('c]'))), $method->invoke(null, + array(0,0,0,0,0,0,'a [b c]'), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + )); + $this->assertEquals(array(false, array(array('a'), array('b c'))), $method->invoke(null, + array(0,0,0,0,0,0,'a [b c]'), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 0, 'noesc' => 0)) + )); + $this->assertEquals(array(false, array(array('a'), array('b c'))), $method->invoke(null, + array(0,0,0,0,0,0,'a [b c]'), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + )); + $this->assertEquals(array(false, array(array('a'), 'q' => array('b c'))), $method->invoke(null, + array(0,0,0,0,0,0,'a q=[b c]'), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + )); + $this->assertEquals(array(false, array(array('a'), array('q=[b c'))), $method->invoke(null, + array(0,0,0,0,0,0,'a [q=[b c]'), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + )); + $this->assertEquals(array(false, array(array('a'), 'q' => array('[b'), array('c]'))), $method->invoke(null, + array(0,0,0,0,0,0,'a q=[b c]'), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + )); + $this->assertEquals(array(false, array(array('a'), 'q' => array('b'), array('c'))), $method->invoke(null, + array(0,0,0,0,0,0,'a [q]=b c'), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + )); + $this->assertEquals(array(false, array(array('a'), 'q' => array('"b c"'))), $method->invoke(null, + array(0,0,0,0,0,0,'a q="b c"'), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + )); + $this->assertEquals(array(false, array(array('(foo bar)'))), $method->invoke(null, + array(0,0,0,0,0,0,'(foo bar)'), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0)) + )); + $this->assertEquals(array(false, array(array('foo'), array("'=='"), array('bar'))), $method->invoke(null, + array(0,0,0,0,0,0,"foo '==' bar"), array('flags' => array('advar' => 1, 'namev' => 1)) + )); + } + /** + * @covers LightnCandy::tokenString + */ + public function testOn_tokenString() { + $method = new ReflectionMethod('LightnCandy', 'tokenString'); + $method->setAccessible(true); + $this->assertEquals('b', $method->invoke(null, + array(0, 'a', 'b', 'c'), 1 + )); + $this->assertEquals('c', $method->invoke(null, + array(0, 'a', 'b', 'c', 'd', 'e') + )); + } + /** + * @covers LightnCandy::validateStartEnd + */ + public function testOn_validateStartEnd() { + $method = new ReflectionMethod('LightnCandy', 'validateStartEnd'); + $method->setAccessible(true); + $this->assertEquals(null, $method->invoke(null, + array_fill(0, 9, ''), array() + )); + $this->assertEquals(null, $method->invoke(null, + array_fill(0, 9, '}}'), array() + )); + $this->assertEquals(true, $method->invoke(null, + array_fill(0, 9, '{{{'), array() + )); + } + /** + * @covers LightnCandy::validateOperations + */ + public function testOn_validateOperations() { + $method = new ReflectionMethod('LightnCandy', 'validateOperations'); + $method->setAccessible(true); + $this->assertEquals(null, $method->invoke(null, + array(0, 0, 0, 0, 0, ''), array(), array() + )); + $this->assertEquals(2, $method->invoke(null, + array(0, 0, 0, 0, 0, '^', '...'), array('usedFeature' => array('isec' => 1), 'level' => 0), array(array('foo')) + )); + $this->assertEquals(3, $method->invoke(null, + array(0, 0, 0, 0, 0, '!', '...'), array('usedFeature' => array('comment' => 2)), array() + )); + $this->assertEquals(true, $method->invoke(null, + array(0, 0, 0, 0, 0, '/'), array('stack' => array(1), 'level' => 1), array() + )); + $this->assertEquals(4, $method->invoke(null, + array(0, 0, 0, 0, 0, '#', '...'), array('usedFeature' => array('sec' => 3), 'level' => 0), array(array('x')) + )); + $this->assertEquals(5, $method->invoke(null, + array(0, 0, 0, 0, 0, '#', '...'), array('usedFeature' => array('if' => 4), 'level' => 0), array(array('if')) + )); + $this->assertEquals(6, $method->invoke(null, + array(0, 0, 0, 0, 0, '#', '...'), array('usedFeature' => array('with' => 5), 'level' => 0, 'flags' => array('with' => 1)), array(array('with')) + )); + $this->assertEquals(7, $method->invoke(null, + array(0, 0, 0, 0, 0, '#', '...'), array('usedFeature' => array('each' => 6), 'level' => 0), array(array('each')) + )); + $this->assertEquals(8, $method->invoke(null, + array(0, 0, 0, 0, 0, '#', '...'), array('usedFeature' => array('unless' => 7), 'level' => 0), array(array('unless')) + )); + $this->assertEquals(9, $method->invoke(null, + array(0, 0, 0, 0, 0, '#', '...'), array('blockhelpers' => array('abc' => ''), 'usedFeature' => array('bhelper' => 8), 'level' => 0), array(array('abc')) + )); + $this->assertEquals(10, $method->invoke(null, + array(0, 0, 0, 0, 0, ' ', '...'), array('usedFeature' => array('delimiter' => 9), 'level' => 0), array() + )); + $this->assertEquals(11, $method->invoke(null, + array(0, 0, 0, 0, 0, '#', '...'), array('hbhelpers' => array('abc' => ''), 'usedFeature' => array('hbhelper' => 10), 'level' => 0), array(array('abc')) + )); + $this->assertEquals(true, $method->invoke(null, + array(0, 0, 0, 0, 0, '>', '...'), array('basedir' => array('.'), 'fileext' => array('.tmpl'), 'usedFeature' => array('unless' => 7, 'partial' => 7), 'level' => 0, 'flags' => array('skippartial' => 0)), array('test') + )); + } + /** + * @covers LightnCandy::addUsageCount + */ + public function testOn_addUsageCount() { + $method = new ReflectionMethod('LightnCandy', 'addUsageCount'); + $method->setAccessible(true); + $this->assertEquals(1, $method->invoke(null, + array('usedCount' => array('test' => array())), 'test', 'testname' + )); + $this->assertEquals(3, $method->invoke(null, + array('usedCount' => array('test' => array('testname' => 2))), 'test', 'testname' + )); + $this->assertEquals(5, $method->invoke(null, + array('usedCount' => array('test' => array('testname' => 2))), 'test', 'testname', 3 + )); + } +} +?>
\ No newline at end of file diff --git a/vendor/zordius/lightncandy/tests/errorTest.php b/vendor/zordius/lightncandy/tests/errorTest.php new file mode 100644 index 00000000..30ecd97d --- /dev/null +++ b/vendor/zordius/lightncandy/tests/errorTest.php @@ -0,0 +1,410 @@ +<?php + +require_once('src/lightncandy.php'); +require_once('tests/helpers_for_test.php'); + +$tmpdir = sys_get_temp_dir(); +$errlog_fn = tempnam($tmpdir, 'terr_'); + +function start_catch_error_log() { + global $errlog_fn; + date_default_timezone_set('GMT'); + if (file_exists($errlog_fn)) { + unlink($errlog_fn); + } + return ini_set('error_log', $errlog_fn); +} + +function stop_catch_error_log() { + global $errlog_fn; + ini_restore('error_log'); + if (!file_exists($errlog_fn)) { + return null; + } + return array_map(function ($l) { + $l = rtrim($l); + preg_match('/GMT\] (.+)/', $l, $m); + return isset($m[1]) ? $m[1] : $l; + }, file($errlog_fn)); +} + +class errorTest extends PHPUnit_Framework_TestCase +{ + public function testException() + { + $this->setExpectedException('Exception', 'Bad token {{{foo}} ! Do you mean {{foo}} or {{{foo}}}?'); + $php = LightnCandy::compile('{{{foo}}', Array('flags' => LightnCandy::FLAG_ERROR_EXCEPTION)); + } + + public function testErrorLog() + { + start_catch_error_log(); + $php = LightnCandy::compile('{{{foo}}', Array('flags' => LightnCandy::FLAG_ERROR_LOG)); + $e = stop_catch_error_log(); + if ($e) { + $this->assertEquals(Array('Bad token {{{foo}} ! Do you mean {{foo}} or {{{foo}}}?'), $e); + } else { + $this->markTestIncomplete('skip HHVM'); + } + } + + /** + * @dataProvider renderErrorProvider + */ + public function testRenderingException($test) + { + $this->setExpectedException('Exception', $test['expected']); + $php = LightnCandy::compile($test['template'], $test['options']); + $renderer = LightnCandy::prepare($php); + $renderer(null, LCRun3::DEBUG_ERROR_EXCEPTION); + } + + /** + * @dataProvider renderErrorProvider + */ + public function testRenderingErrorLog($test) + { + start_catch_error_log(); + $php = LightnCandy::compile($test['template'], $test['options']); + $renderer = LightnCandy::prepare($php); + $renderer(null, LCRun3::DEBUG_ERROR_LOG); + $e = stop_catch_error_log(); + if ($e) { + $this->assertEquals(Array($test['expected']), $e); + } else { + $this->markTestIncomplete('skip HHVM'); + } + } + + public function renderErrorProvider() + { + $errorCases = Array( + Array( + 'template' => '{{{foo}}}', + 'expected' => 'LCRun3: [foo] is not exist', + ), + Array( + 'template' => '{{foo}}', + 'options' => Array( + 'hbhelpers' => Array( + 'foo' => function () { + return 1/0; + } + ), + ), + 'expected' => 'LCRun3: call custom helper \'foo\' error: Division by zero', + ), + ); + + return array_map(function($i) { + if (!isset($i['options'])) { + $i['options'] = Array('flags' => LightnCandy::FLAG_RENDER_DEBUG); + } + if (!isset($i['options']['flags'])) { + $i['options']['flags'] = LightnCandy::FLAG_RENDER_DEBUG; + } + return Array($i); + }, $errorCases); + } + + /** + * @dataProvider errorProvider + */ + public function testErrors($test) + { + global $tmpdir; + + $php = LightnCandy::compile($test['template'], $test['options']); + $context = LightnCandy::getContext(); + + // This case should be compiled without error + if (!isset($test['expected'])) { + return; + } + + $this->assertEquals($test['expected'], $context['error'], "Code: $php"); + } + + public function errorProvider() + { + $errorCases = Array( + Array( + 'template' => '{{testerr1}}}', + 'expected' => 'Bad token {{testerr1}}} ! Do you mean {{testerr1}} or {{{testerr1}}}?', + ), + Array( + 'template' => '{{{testerr2}}', + 'expected' => 'Bad token {{{testerr2}} ! Do you mean {{testerr2}} or {{{testerr2}}}?', + ), + Array( + 'template' => '{{{#testerr3}}}', + 'expected' => 'Bad token {{{#testerr3}}} ! Do you mean {{#testerr3}} ?', + ), + Array( + 'template' => '{{{!testerr4}}}', + 'expected' => 'Bad token {{{!testerr4}}} ! Do you mean {{!testerr4}} ?', + ), + Array( + 'template' => '{{{^testerr5}}}', + 'expected' => 'Bad token {{{^testerr5}}} ! Do you mean {{^testerr5}} ?', + ), + Array( + 'template' => '{{{/testerr6}}}', + 'expected' => 'Bad token {{{/testerr6}}} ! Do you mean {{/testerr6}} ?', + ), + Array( + 'template' => '{{win[ner.test1}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming in {{win[ner.test1}}', + ), + Array( + 'template' => '{{win]ner.test2}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming as \'win]ner.test2\' in {{win]ner.test2}} !', + ), + Array( + 'template' => '{{wi[n]ner.test3}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming as \'wi[n]ner.test3\' in {{wi[n]ner.test3}} !', + ), + Array( + 'template' => '{{winner].[test4]}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming as \'winner].[test4]\' in {{winner].[test4]}} !', + ), + Array( + 'template' => '{{winner[.test5]}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming as \'winner[.test5]\' in {{winner[.test5]}} !', + ), + Array( + 'template' => '{{winner.[.test6]}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + ), + Array( + 'template' => '{{winner.[#te.st7]}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + ), + Array( + 'template' => '{{test8}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + ), + Array( + 'template' => '{{test9]}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming as \'test9]\' in {{test9]}} !', + ), + Array( + 'template' => '{{testA[}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming as \'testA[\' in {{testA[}} !', + ), + Array( + 'template' => '{{[testB}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming in {{[testB}}', + ), + Array( + 'template' => '{{]testC}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming as \']testC\' in {{]testC}} !', + ), + Array( + 'template' => '{{[testD]}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + ), + Array( + 'template' => '{{te]stE}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming as \'te]stE\' in {{te]stE}} !', + ), + Array( + 'template' => '{{tee[stF}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming in {{tee[stF}}', + ), + Array( + 'template' => '{{te.e[stG}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming in {{te.e[stG}}', + ), + Array( + 'template' => '{{te.e]stH}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming as \'te.e]stH\' in {{te.e]stH}} !', + ), + Array( + 'template' => '{{te.e[st.endI}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming in {{te.e[st.endI}}', + ), + Array( + 'template' => '{{te.e]st.endJ}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming as \'te.e]st.endJ\' in {{te.e]st.endJ}} !', + ), + Array( + 'template' => '{{te.[est].endK}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + ), + Array( + 'template' => '{{te.t[est].endL}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming as \'te.t[est].endL\' in {{te.t[est].endL}} !', + ), + Array( + 'template' => '{{te.t[est]o.endM}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming as \'te.t[est]o.endM\' in {{te.t[est]o.endM}} !', + ), + Array( + 'template' => '{{te.[est]o.endN}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + 'expected' => 'Wrong variable naming as \'te.[est]o.endN\' in {{te.[est]o.endN}} !', + ), + Array( + 'template' => '{{te.[e.st].endO}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + ), + Array( + 'template' => '{{te.[e.s[t].endP}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + ), + Array( + 'template' => '{{te.[e[s.t].endQ}}', + 'options' => Array('flags' => LightnCandy::FLAG_ADVARNAME), + ), + Array( + 'template' => '{{helper}}', + 'options' => Array('helpers' => Array( + 'helper' => Array('bad input'), + )), + 'expected' => 'I found an array in helpers with key as helper, please fix it.', + ), + Array( + 'template' => '<ul>{{#each item}}<li>{{name}}</li>', + 'expected' => 'Unclosed token {{{#each item}}} !!', + ), + Array( + 'template' => 'issue63: {{test_join}} Test! {{this}} {{/test_join}}', + 'expected' => 'Unexpect token: {{/test_join}} !', + ), + Array( + 'template' => '{{#if a}}TEST{{/with}}', + 'options' => Array('flags' => LightnCandy::FLAG_WITH), + 'expected' => 'Unexpect token: {{/with}} !', + ), + Array( + 'template' => '{{#foo}}error{{/bar}}', + 'expected' => 'Unexpect token {{/bar}} ! Previous token {{#[foo]}} is not closed', + ), + Array( + 'template' => '{{../foo}}', + 'expected' => 'Do not support {{../var}}, you should do compile with LightnCandy::FLAG_PARENT flag', + ), + Array( + 'template' => '{{..}}', + 'expected' => 'Do not support {{../var}}, you should do compile with LightnCandy::FLAG_PARENT flag', + ), + Array( + 'template' => '{{test_join [a]=b}}', + 'options' => Array( + 'flags' => LightnCandy::FLAG_NAMEDARG, + 'helpers' => Array('test_join') + ), + 'expected' => "Wrong argument name as '[a]' in {{test_join [a]=b}} ! You should fix your template or compile with LightnCandy::FLAG_ADVARNAME flag.", + ), + Array( + 'template' => '{{a=b}}', + 'options' => Array('flags' => LightnCandy::FLAG_NAMEDARG), + 'expected' => 'Do not support name=value in {{a=b}}, you should use it after a custom helper.', + ), + Array( + 'template' => '{{test a=b}}', + 'options' => Array('flags' => LightnCandy::FLAG_NAMEDARG), + 'expected' => 'Do not support name=value in {{test a=b}}, maybe you missing the custom helper?', + ), + Array( + 'template' => '{{#test a=b}}YA~{{/test}}', + 'options' => Array('flags' => LightnCandy::FLAG_NAMEDARG), + 'expected' => 'Do not support name=value in {{#test a=b}}, maybe you missing the block custom helper?', + ), + Array( + 'template' => '{{#foo}}1{{^}}2{{/foo}}', + 'expected' => 'Do not support {{^}}, you should do compile with LightnCandy::FLAG_ELSE flag', + ), + Array( + 'template' => '{{#with a}OK!{{/with}}', + 'options' => Array('flags' => LightnCandy::FLAG_WITH), + 'expected' => 'Unclosed token {{{#with a}OK!{{/with}}} !!', + ), + Array( + 'template' => '{{#each a}OK!{{/each}}', + 'expected' => 'Unclosed token {{{#each a}OK!{{/each}}} !!', + ), + Array( + 'template' => '{{#with items}}OK!{{/with}}', + 'options' => Array('flags' => LightnCandy::FLAG_WITH), + ), + Array( + 'template' => '{{#with}}OK!{{/with}}', + 'options' => Array('flags' => LightnCandy::FLAG_WITH), + 'expected' => 'No argument after {{#with}} !', + ), + Array( + 'template' => '{{>not_found}}', + 'expected' => "Can not find partial file for 'not_found', you should set correct basedir and fileext in options", + ), + Array( + 'template' => '{{>tests/test1 foo}}', + 'options' => Array('basedir' => '.'), + 'expected' => 'Do not support {{>tests/test1 [foo]}}, you should do compile with LightnCandy::FLAG_RUNTIMEPARTIAL flag', + ), + Array( + 'template' => '{{#with foo}}ABC{{/with}}', + 'expected' => 'Do not support {{#with var}}, you should do compile with LightnCandy::FLAG_WITH flag', + ), + Array( + 'template' => '{{abc}}', + 'options' => Array('helpers' => Array('abc')), + 'expected' => 'Can not find custom helper function defination abc() !', + ), + Array( + 'template' => '{{=~= =~=}}', + 'expected' => "Can not set delimiter contains '=' , you try to set delimiter as '~=' and '=~'.", + ), + Array( + 'template' => '{{>recursive}}', + 'options' => Array('basedir' => 'tests', 'flags' => LightnCandy::FLAG_WITH), + 'expected' => Array( + 'I found recursive partial includes as the path: recursive -> recursive! You should fix your template or compile with LightnCandy::FLAG_RUNTIMEPARTIAL flag.', + "Skip rendering partial 'recursive' again due to recursive detected", + ) + ), + Array( + 'template' => '{{test_join (foo bar)}}', + 'options' => Array( + 'flags' => LightnCandy::FLAG_ADVARNAME, + 'helpers' => Array('test_join'), + ), + 'expected' => "Custom helper 'foo' not found!", + ), + ); + + return array_map(function($i) { + if (!isset($i['options'])) { + $i['options'] = Array('flags' => 0); + } + if (!isset($i['options']['flags'])) { + $i['options']['flags'] = 0; + } + if (isset($i['expected']) && !is_array($i['expected'])) { + $i['expected'] = Array($i['expected']); + } + return Array($i); + }, $errorCases); + } +} + + +?> diff --git a/vendor/zordius/lightncandy/tests/example_debug.php b/vendor/zordius/lightncandy/tests/example_debug.php new file mode 100644 index 00000000..bc573e0d --- /dev/null +++ b/vendor/zordius/lightncandy/tests/example_debug.php @@ -0,0 +1,28 @@ +<?php +require('src/lightncandy.php'); + +$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}} +"; + +$php = LightnCandy::compile($template, Array( + 'flags' => LightnCandy::FLAG_RENDER_DEBUG | LightnCandy::FLAG_HANDLEBARSJS +)); + +$renderer = LightnCandy::prepare($php); +error_reporting(0); +echo $renderer(Array('name' => 'John'), LCRun3::DEBUG_TAGS_ANSI); +?> diff --git a/vendor/zordius/lightncandy/tests/example_helpers.php b/vendor/zordius/lightncandy/tests/example_helpers.php new file mode 100644 index 00000000..f5e67d6f --- /dev/null +++ b/vendor/zordius/lightncandy/tests/example_helpers.php @@ -0,0 +1,58 @@ +<?php + +// Custom Helper Interface ... noname arguments +// Template: {{helper1 article.url article.text}} +function helper1 ($args, $named) { + $u = (isset($args[0])) ? $args[0] : 'undefined'; + $t = (isset($args[1])) ? $args[1] : 'undefined'; + return "<a href=\"{$u}\">{$t}</a>"; +} + +// Custom Helper Interface ... named arguments +// Template: {{helper1 url=article.url text=article.text [ur"l]=article.extra}} +function helper2 ($args, $named) { + $u = isset($named['url']) ? jsraw($named['url']) : 'undefined'; + $t = isset($named['text']) ? jsraw($named['text']) : 'undefined'; + $x = isset($named['ur"l']) ? $named['ur"l'] : 'undefined'; + return "<a href=\"{$u}\">{$t}</a>({$x})"; +} + +// Block Custom Helper Interface ... +// Template: {{helper3 articles}} +function helper3 ($cx, $args, $named) { + return Array('test1', 'test2', 'test3'); +} + +// Block Custom Helper Interface ... +// Template: {{helper3 val=values odd=enable_odd}} +function helper4 ($cx, $args, $named) { + if (isset($named['val']) && is_array($cx)) { + $cx['helper4_value'] = $named['val'] % 2; + return $cx; + } + if (isset($named['odd'])) { + return Array(1,3,5,7,9); + } +} + +// Handlebars.js Custom Helper Interface ... +// Template: {{#myeach articles}}Article: ....{{/myeach}} +function myeach ($list, $options) { + foreach ($list as $item) { + $ret .= $options['fn']($item); + } + return $ret; +} + +// Simulate Javascript toString() behaviors +function jsraw ($i) { + if ($i === true) { + return 'true'; + } + if ($i === false) { + return 'false'; + } + return $i; +} + +?> diff --git a/vendor/zordius/lightncandy/tests/handlebarsSpecTest.php b/vendor/zordius/lightncandy/tests/handlebarsSpecTest.php new file mode 100644 index 00000000..5124b226 --- /dev/null +++ b/vendor/zordius/lightncandy/tests/handlebarsSpecTest.php @@ -0,0 +1,162 @@ +<?php + +require_once('src/lightncandy.php'); + +$tmpdir = sys_get_temp_dir(); + +class HandlebarsSpecTest extends PHPUnit_Framework_TestCase +{ + /** + * @dataProvider jsonSpecProvider + */ + public function testSpecs($spec) + { + global $tmpdir; + + //// Skip bad specs + // No expect in spec + if (!isset($spec['expected'])) { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , no expected result in spec, skip."); + } + // This spec is bad , lightncandy result as '} hello }' and same with mustache.js + if ($spec['template'] === '{{{{raw}}}} {{test}} {{{{/raw}}}}') { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , bad spec, skip."); + } + // missing partial in this spec + if ($spec['it'] === 'rendering function partial in vm mode') { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , bad spec, skip."); + } + // Helper depend on an external class, skip it now. + if ($spec['it'] === 'simple literals work') { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , external class not found, skip."); + } + // partial not found: global_test + if ($spec['message'] === 'Partials can use globals or passed') { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , partial not found, skip."); + } + // lambda not found in spec + if ($spec['it'] === "bug reported by @fat where lambdas weren't being properly resolved") { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , lambda not found, skip."); + } + + //// Skip unsupported features + // can not get any hint of 'function' from handlebars-spec , maybe it is a conversion error. + if (($spec['description'] === 'basic context') && preg_match('/functions/', $spec['it'])) { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , undefined function in spec json, skip."); + } + if (preg_match('/(.+) with function argument/', $spec['it'])) { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , undefined function in spec json, skip."); + } + if ($spec['it'] === 'Functions are bound to the context in knownHelpers only mode') { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , undefined function in spec json, skip."); + } + if ($spec['it'] === 'lambdas are resolved by blockHelperMissing, not handlebars proper') { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , undefined function in spec json, skip."); + } + if ($spec['description'] === '#SafeString') { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , undefined function in spec json, skip."); + } + + // Do not support includeZero now + if (($spec['description'] === '#if') && preg_match('/includeZero=true/', $spec['template'])) { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , lightncandy do not support this now."); + } + + // Do not support setting options.data now + if ($spec['it'] === 'data passed to helpers') { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , lightncandy do not support this now."); + } + + // Do not support buildin helper : lookup now + if ($spec['description'] == '#lookup') { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , lightncandy do not support this now."); + } + + // Lightncandy will not support old path style as foo/bar , now only support foo.bar . + if ($spec['it'] === 'literal paths') { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , lightncandy do not support this now."); + } + + // Do not support {{~{foo}~}} , use {{{~foo~}}} + if ($spec['template'] === ' {{~{foo}~}} ') { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , lightncandy do not support this now."); + } + + // setup helpers + $helpers = Array(); + if (isset($spec['helpers'])) { + foreach ($spec['helpers'] as $name => $func) { + if (!isset($func['php'])) { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , no PHP helper code provided for this case."); + } + + // Wrong PHP helper interface in spec, skip. + preg_match('/function\s*\(.+?\)/', isset($func['javascript']) ? $func['javascript'] : '', $js_args); + preg_match('/function\s*\(.+?\)/', $func['php'], $php_args); + $jsn = isset($js_args[0]) ? substr_count($js_args[0], ',') : 0; + $phpn = isset($php_args[0]) ? substr_count($php_args[0], ',') : 0; + if ($jsn !== $phpn) { + $this->markTestIncomplete("Skip [{$spec['file']}#{$spec['description']}]#{$spec['no']} , PHP helper interface is wrong."); + } + + $hname = "custom_helper_{$spec['no']}_$name"; + $helpers[$name] = $hname; + eval(preg_replace('/function/', "function $hname", $func['php'], 1)); + } + + } + + if (($spec['it'] === 'tokenizes hash arguments') || ($spec['it'] === 'tokenizes special @ identifiers')) { + $helpers['foo'] = function () {return 'ABC';}; + } + + $flag = LightnCandy::FLAG_HANDLEBARSJS | LightnCandy::FLAG_ERROR_EXCEPTION | LightnCandy::FLAG_RUNTIMEPARTIAL | LightnCandy::FLAG_EXTHELPER | LightnCandy::FLAG_ERROR_SKIPPARTIAL | LightnCandy::FLAG_EXTHELPER; + + foreach (Array($flag, $flag | LightnCandy::FLAG_STANDALONE) as $f) { + try { + $php = LightnCandy::compile($spec['template'], Array( + 'flags' => $f, + 'hbhelpers' => $helpers, + 'basedir' => $tmpdir, + 'partials' => isset($spec['partials']) ? $spec['partials'] : null, + )); + } catch (Exception $e) { + if (($spec['description'] === 'Tokenizer') && preg_match('/tokenizes inverse .+ as "OPEN_INVERSE.+CLOSE"/', $spec['it'])) { + continue; + } + print_r(LightnCandy::getContext()); + $this->fail('Exception:' . $e->getMessage()); + } + $renderer = LightnCandy::prepare($php); + if ($spec['description'] === 'Tokenizer') { + // no compile error means passed + continue; + } + $this->assertEquals($spec['expected'], $renderer($spec['data']), "[{$spec['file']}#{$spec['description']}]#{$spec['no']}:{$spec['it']} PHP CODE: $php"); + } + } + + public function jsonSpecProvider() + { + $ret = Array(); + + foreach (glob('specs/handlebars/spec/*.json') as $file) { + $i=0; + $json = json_decode(file_get_contents($file), true); + $ret = array_merge($ret, array_map(function ($d) use ($file, &$i) { + $d['file'] = $file; + $d['no'] = ++$i; + if (!isset($d['message'])) { + $d['message'] = null; + } + if (!isset($d['data'])) { + $d['data'] = null; + } + return Array($d); + }, $json)); + } + + return $ret; + } +} +?> diff --git a/vendor/zordius/lightncandy/tests/helpers_for_test.php b/vendor/zordius/lightncandy/tests/helpers_for_test.php new file mode 100644 index 00000000..93a1dbc7 --- /dev/null +++ b/vendor/zordius/lightncandy/tests/helpers_for_test.php @@ -0,0 +1,137 @@ +<?php + +// Classes for inputs or helpers +class myClass { + function test() { + return 'testMethod OK!'; + } + + function helper2($arg) { + return "=$arg="; + } + + function __call($method, $args) { + return "-- $method:" . print_r($args, true); + } +} + +class foo { + public $prop = 'Yes!'; + + function bar() { + return 'OK!'; + } +} + +class twoDimensionIterator implements Iterator { + private $position = 0; + private $x = 0; + private $y = 0; + private $w = 0; + private $h = 0; + + public function __construct($w, $h) { + $this->w = $w; + $this->h = $h; + $this->rewind(); + } + + function rewind() { + $this->position = 0; + $this->x = 0; + $this->y = 0; + } + + function current() { + return $this->x * $this->y; + } + + function key() { + return $this->x . 'x' . $this->y; + } + + function next() { + ++$this->position; + $this->x = $this->position % $this->w; + $this->y = floor($this->position / $this->w); + } + + function valid() { + return $this->position < $this->w * $this->h; + } +} + +// Custom helpers +function helper1($arg) { + return "-$arg-"; +} +function alink($u, $t) { + return "<a href=\"$u\">$t</a>"; +} + + function meetup_date_format() { + return "OKOK~1"; +} + +function meetup_date_format2() { + return "OKOK~2"; +} + +function meetup_date_format3 () { + return "OKOK~3"; +} + +function meetup_date_format4(){ + return "OKOK~4";}; + + +function test_array ($input) { + return is_array($input[0]) ? 'IS_ARRAY' : 'NOT_ARRAY'; +} + +function test_join ($input) { + return join('.', $input[0]); +} + +// Custom helpers for handlebars (should be used in hbhelpers) +function myif ($conditional, $options) { + if ($conditional) { + return $options['fn'](); + } else { + return $options['inverse'](); + } +} + +function mywith ($context, $options) { + return $options['fn']($context); +} + +function myeach ($context, $options) { + $ret = ''; + foreach ($context as $cx) { + $ret .= $options['fn']($cx); + } + return $ret; +} + +function mylogic ($input, $yes, $no, $options) { + if ($input === true) { + return $options['fn']($yes); + } else { + return $options['inverse']($no); + } +} + +function mydash ($a, $b) { + return "$a-$b"; +} + +function myjoin ($a, $b) { + return "$a$b"; +} + +function getroot ($options) { + return $options['data']['root']; +} + +?> diff --git a/vendor/zordius/lightncandy/tests/mustacheSpecTest.php b/vendor/zordius/lightncandy/tests/mustacheSpecTest.php new file mode 100644 index 00000000..158f82cf --- /dev/null +++ b/vendor/zordius/lightncandy/tests/mustacheSpecTest.php @@ -0,0 +1,53 @@ +<?php + +require_once('src/lightncandy.php'); + +$tmpdir = sys_get_temp_dir(); + +class MustacheSpecTest extends PHPUnit_Framework_TestCase +{ + /** + * @dataProvider jsonSpecProvider + */ + public function testSpecs($spec) + { + global $tmpdir; + + $flag = LightnCandy::FLAG_MUSTACHE | LightnCandy::FLAG_ERROR_EXCEPTION | LightnCandy::FLAG_RUNTIMEPARTIAL; + + foreach (Array($flag, $flag | LightnCandy::FLAG_STANDALONE) as $f) { + $php = LightnCandy::compile($spec['template'], Array( + 'flags' => $f, + 'partials' => isset($spec['partials']) ? $spec['partials'] : null, + 'basedir' => $tmpdir, + )); + $renderer = LightnCandy::prepare($php); + $this->assertEquals($spec['expected'], $renderer($spec['data']), "[{$spec['file']}.{$spec['name']}]#{$spec['no']}:{$spec['desc']} PHP CODE: $php"); + } + } + + public function jsonSpecProvider() + { + $ret = Array(); + + foreach (glob('specs/mustache/specs/*.json') as $file) { + // Skip lambda extension + if (preg_match('/lambdas\\.json$/', $file)) { + continue; + } + + $i=0; + $json = json_decode(file_get_contents($file), true); + $ret = array_merge($ret, array_map(function ($d) use ($file, &$i) { + $d['file'] = $file; + $d['no'] = ++$i; + return Array($d); + }, $json['tests'])); + } + + return $ret; + } +} + + +?> diff --git a/vendor/zordius/lightncandy/tests/recursive.tmpl b/vendor/zordius/lightncandy/tests/recursive.tmpl new file mode 100644 index 00000000..9e3290da --- /dev/null +++ b/vendor/zordius/lightncandy/tests/recursive.tmpl @@ -0,0 +1 @@ +{{#if foo}}{{bar}} -> {{#with foo}}{{>recursive}}{{/with}}{{else}}END!{{/if}} diff --git a/vendor/zordius/lightncandy/tests/regressionTest.php b/vendor/zordius/lightncandy/tests/regressionTest.php new file mode 100644 index 00000000..bf591536 --- /dev/null +++ b/vendor/zordius/lightncandy/tests/regressionTest.php @@ -0,0 +1,736 @@ +<?php + +require_once('src/lightncandy.php'); +require_once('tests/helpers_for_test.php'); + +$tmpdir = sys_get_temp_dir(); + +class regressionTest extends PHPUnit_Framework_TestCase +{ + /** + * @dataProvider issueProvider + */ + public function testIssues($issue) + { + global $tmpdir; + + $php = LightnCandy::compile($issue['template'], isset($issue['options']) ? $issue['options'] : null); + $context = LightnCandy::getContext(); + if (count($context['error'])) { + $this->fail('Compile failed due to:' . print_r($context['error'], true)); + } + $renderer = LightnCandy::prepare($php); + + $this->assertEquals($issue['expected'], $renderer($issue['data'], $issue['debug']), "PHP CODE:\n$php"); + } + + public function issueProvider() + { + $issues = Array( + Array( + 'id' => 39, + 'template' => '{{{tt}}}', + 'options' => null, + 'data' => Array('tt' => 'bla bla bla'), + 'expected' => 'bla bla bla' + ), + + Array( + 'id' => 44, + 'template' => '<div class="terms-text"> {{render "artists-terms"}} </div>', + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS | LightnCandy::FLAG_ERROR_LOG | LightnCandy::FLAG_EXTHELPER, + 'helpers' => Array( + 'url', + 'render' => function($view,$data = array()) { + return 'OK!'; + } + ) + ), + 'data' => Array('tt' => 'bla bla bla'), + 'expected' => '<div class="terms-text"> OK! </div>' + ), + + Array( + 'id' => 45, + 'template' => '{{{a.b.c}}}, {{a.b.bar}}, {{a.b.prop}}', + 'options' => Array( + 'flags' => LightnCandy::FLAG_ERROR_LOG | LightnCandy::FLAG_INSTANCE | LightnCandy::FLAG_HANDLEBARSJS, + ), + 'data' => Array('a' => Array('b' => new foo)), + 'expected' => ', OK!, Yes!' + ), + + Array( + 'id' => 46, + 'template' => '{{{this.id}}}, {{a.id}}', + 'options' => Array( + 'flags' => LightnCandy::FLAG_THIS, + ), + 'data' => Array('id' => 'bla bla bla', 'a' => Array('id' => 'OK!')), + 'expected' => 'bla bla bla, OK!' + ), + + Array( + 'id' => 49, + 'template' => '{{date_format}} 1, {{date_format2}} 2, {{date_format3}} 3, {{date_format4}} 4', + 'options' => Array( + 'helpers' => Array( + 'date_format' => 'meetup_date_format', + 'date_format2' => 'meetup_date_format2', + 'date_format3' => 'meetup_date_format3', + 'date_format4' => 'meetup_date_format4' + ) + ), + 'data' => null, + 'expected' => 'OKOK~1 1, OKOK~2 2, OKOK~3 3, OKOK~4 4' + ), + + Array( + 'id' => 52, + 'template' => '{{{test_array tmp}}} should be happy!', + 'options' => Array( + 'helpers' => Array( + 'test_array', + ) + ), + 'data' => Array('tmp' => Array('A', 'B', 'C')), + 'expected' => 'IS_ARRAY should be happy!' + ), + + Array( + 'id' => 62, + 'template' => '{{{test_join @root.foo.bar}}} should be happy!', + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS | LightnCandy::FLAG_ERROR_EXCEPTION, + 'helpers' => array('test_join') + ), + 'data' => Array('foo' => Array('A', 'B', 'bar' => Array('C', 'D'))), + 'expected' => 'C.D should be happy!', + ), + + Array( + 'id' => 64, + 'template' => '{{#each foo}} Test! {{this}} {{/each}}{{> test1}} ! >>> {{>recursive}}', + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS | LightnCandy::FLAG_RUNTIMEPARTIAL, + 'basedir' => 'tests', + ), + 'data' => Array( + 'bar' => 1, + 'foo' => Array( + 'bar' => 3, + 'foo' => Array( + 'bar' => 5, + 'foo' => Array( + 'bar' => 7, + 'foo' => Array( + 'bar' => 11, + 'foo' => Array( + 'no foo here' + ) + ) + ) + ) + ) + ), + 'expected' => " Test! 3 Test! [object Object] 123\n ! >>> 1 -> 3 -> 5 -> 7 -> 11 -> END!\n\n\n\n\n\n", + ), + + Array( + 'id' => 66, + 'template' => '{{&foo}} , {{foo}}, {{{foo}}}', + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS + ), + 'data' => Array('foo' => 'Test & " \' :)'), + 'expected' => 'Test & " \' :) , Test & " ' :), Test & " \' :)', + ), + + Array( + 'id' => 68, + 'template' => '{{#myeach foo}} Test! {{this}} {{/myeach}}', + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS, + 'hbhelpers' => Array( + 'myeach' => function ($context, $options) { + $ret = ''; + foreach ($context as $cx) { + $ret .= $options['fn']($cx); + } + return $ret; + } + ) + ), + 'data' => Array('foo' => Array('A', 'B', 'bar' => Array('C', 'D', 'E'))), + 'expected' => ' Test! A Test! B Test! C,D,E ', + ), + + Array( + 'id' => 81, + 'template' => '{{#with ../person}} {{^name}} Unknown {{/name}} {{/with}}?!', + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS | LightnCandy::FLAG_ERROR_EXCEPTION, + ), + 'data' => Array('parent?!' => Array('A', 'B', 'bar' => Array('C', 'D', 'E'))), + 'expected' => '?!' + ), + + Array( + 'id' => 83, + 'template' => '{{> tests/test1}}', + 'options' => Array( + 'basedir' => '.', + ), + 'data' => null, + 'expected' => "123\n" + ), + + Array( + 'id' => 85, + 'template' => '{{helper 1 foo bar="q"}}', + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS, + 'hbhelpers' => Array( + 'helper' => function ($arg1, $arg2, $options) { + return "ARG1:$arg1, ARG2:$arg2, HASH:{$options['hash']['bar']}"; + } + ) + ), + 'data' => Array('foo' => 'BAR'), + 'expected' => 'ARG1:1, ARG2:BAR, HASH:q', + ), + + Array( + 'id' => 88, + 'template' => '{{>test2}}', + 'options' => Array( + 'flags' => 0, + 'basedir' => 'tests', + ), + 'data' => null, + 'expected' => "a123\nb\n", + ), + + Array( + 'id' => 89, + 'template' => '{{#with}}SHOW:{{.}} {{/with}}', + 'data' => Array('with' => Array(1, 3, 7), 'a' => Array(2, 4, 9)), + 'expected' => 'SHOW:1 SHOW:3 SHOW:7 ', + ), + + Array( + 'id' => 90, + 'template' => '{{#items}}{{#value}}{{.}}{{/value}}{{/items}}', + 'data' => Array('items' => Array(Array('value'=>'123'))), + 'expected' => '123', + ), + + Array( + 'id' => 109, + 'template' => '{{#if "OK"}}it\'s great!{{/if}}', + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS | LightnCandy::FLAG_NOESCAPE, + ), + 'data' => null, + 'expected' => 'it\'s great!', + ), + + Array( + 'id' => 110, + 'template' => 'ABC{{#block "YES!"}}DEF{{foo}}GHI{{/block}}JKL', + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS | LightnCandy::FLAG_BESTPERFORMANCE, + 'hbhelpers' => Array( + 'block' => function ($name, $options) { + return "1-$name-2-" . $options['fn']() . '-3'; + } + ), + ), + 'data' => Array('foo' => 'bar'), + 'expected' => 'ABC1-YES!-2-DEFbarGHI-3JKL', + ), + + Array( + 'id' => 109, + 'template' => '{{foo}} {{> test}}', + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS | LightnCandy::FLAG_NOESCAPE, + 'partials' => Array('test' => '{{foo}}'), + ), + 'data' => Array('foo' => '<'), + 'expected' => '< <', + ), + + Array( + 'id' => 114, + 'template' => '{{^myeach .}}OK:{{.}},{{else}}NOT GOOD{{/myeach}}', + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS | LightnCandy::FLAG_BESTPERFORMANCE, + 'hbhelpers' => Array( + 'myeach' => function ($context, $options) { + $ret = ''; + foreach ($context as $cx) { + $ret .= $options['fn']($cx); + } + return $ret; + } + ), + ), + 'data' => Array(1, 'foo', 3, 'bar'), + 'expected' => 'NOT GOODNOT GOODNOT GOODNOT GOOD', + ), + + Array( + 'template' => 'ABC{{#block "YES!"}}DEF{{foo}}GHI{{else}}NO~{{/block}}JKL', + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS | LightnCandy::FLAG_BESTPERFORMANCE, + 'hbhelpers' => Array( + 'block' => function ($name, $options) { + return "1-$name-2-" . $options['fn']() . '-3'; + } + ), + ), + 'data' => Array('foo' => 'bar'), + 'expected' => 'ABC1-YES!-2-DEFbarGHI-3JKL', + ), + + Array( + 'template' => '-{{getroot}}=', + 'options' => Array( + 'flags' => LightnCandy::FLAG_SPVARS, + 'hbhelpers' => Array('getroot'), + ), + 'data' => 'ROOT!', + 'expected' => '-ROOT!=', + ), + + Array( + 'template' => 'A{{#each .}}-{{#each .}}={{.}},{{@key}},{{@index}},{{@../index}}~{{/each}}%{{/each}}B', + 'data' => Array(Array('a' => 'b'), Array('c' => 'd'), Array('e' => 'f')), + 'options' => Array( + 'flags' => LightnCandy::FLAG_PARENT | LightnCandy::FLAG_THIS | LightnCandy::FLAG_SPVARS, + ), + 'expected' => 'A-=b,a,0,0~%-=d,c,0,1~%-=f,e,0,2~%B', + ), + + Array( + 'template' => 'ABC{{#block "YES!"}}TRUE{{else}}DEF{{foo}}GHI{{/block}}JKL', + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS | LightnCandy::FLAG_BESTPERFORMANCE, + 'hbhelpers' => Array( + 'block' => function ($name, $options) { + return "1-$name-2-" . $options['inverse']() . '-3'; + } + ), + ), + 'data' => Array('foo' => 'bar'), + 'expected' => 'ABC1-YES!-2-DEFbarGHI-3JKL', + ), + + Array( + 'template' => '{{#each .}}{{..}}>{{/each}}', + 'data' => Array('a', 'b', 'c'), + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS, + ), + 'expected' => 'a,b,c>a,b,c>a,b,c>', + ), + + Array( + 'template' => '{{#each .}}->{{>tests/test3}}{{/each}}', + 'data' => Array('a', 'b', 'c'), + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS, + 'basedir' => '.', + ), + 'expected' => "->New context:a\n->New context:b\n->New context:c\n", + ), + + Array( + 'template' => '{{#each .}}->{{>tests/test3 ../foo}}{{/each}}', + 'data' => Array('a', 'foo' => Array('d', 'e', 'f')), + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS | LightnCandy::FLAG_RUNTIMEPARTIAL, + 'basedir' => '.', + ), + 'expected' => "->New context:d,e,f\n->New context:d,e,f\n", + ), + + Array( + 'template' => '{{{"{{"}}}', + 'data' => null, + 'expected' => '{{', + ), + + Array( + 'template' => '{{good_helper}}', + 'data' => null, + 'options' => Array( + 'helpers' => Array('good_helper' => 'foo::bar'), + ), + 'expected' => 'OK!', + ), + + Array( + 'template' => '-{{.}}-', + 'options' => Array('flags' => LightnCandy::FLAG_THIS), + 'data' => 'abc', + 'expected' => '-abc-', + ), + + Array( + 'template' => '-{{this}}-', + 'options' => Array('flags' => LightnCandy::FLAG_THIS), + 'data' => 123, + 'expected' => '-123-', + ), + + Array( + 'template' => '{{#if .}}YES{{else}}NO{{/if}}', + 'options' => Array('flags' => LightnCandy::FLAG_ELSE), + 'data' => true, + 'expected' => 'YES', + ), + + Array( + 'template' => '{{foo}}', + 'options' => Array('flags' => LightnCandy::FLAG_RENDER_DEBUG), + 'data' => Array('foo' => 'OK'), + 'expected' => 'OK', + ), + + Array( + 'template' => '{{foo}}', + 'options' => Array('flags' => LightnCandy::FLAG_RENDER_DEBUG), + 'debug' => LCRun3::DEBUG_TAGS_ANSI, + 'data' => Array('foo' => 'OK'), + 'expected' => pack('H*', '1b5b303b33326d7b7b5b666f6f5d7d7d1b5b306d'), + ), + + Array( + 'template' => '{{foo}}', + 'options' => Array('flags' => LightnCandy::FLAG_RENDER_DEBUG), + 'debug' => LCRun3::DEBUG_TAGS_HTML, + 'data' => null, + 'expected' => '<!--MISSED((-->{{[foo]}}<!--))-->', + ), + + Array( + 'template' => '{{#foo}}OK{{/foo}}', + 'options' => Array('flags' => LightnCandy::FLAG_RENDER_DEBUG), + 'debug' => LCRun3::DEBUG_TAGS_HTML, + 'data' => null, + 'expected' => '<!--MISSED((-->{{#[foo]}}<!--))--><!--SKIPPED--><!--MISSED((-->{{/[foo]}}<!--))-->', + ), + + Array( + 'template' => '{{#foo}}OK{{/foo}}', + 'options' => Array('flags' => LightnCandy::FLAG_RENDER_DEBUG), + 'debug' => LCRun3::DEBUG_TAGS_ANSI, + 'data' => null, + 'expected' => pack('H*', '1b5b303b33316d7b7b235b666f6f5d7d7d1b5b306d1b5b303b33336d534b49505045441b5b306d1b5b303b33316d7b7b2f5b666f6f5d7d7d1b5b306d'), + ), + + Array( + 'template' => '{{#myif foo}}YES{{else}}NO{{/myif}}', + 'data' => null, + 'options' => Array( + 'hbhelpers' => Array('myif'), + ), + 'expected' => 'NO', + ), + + Array( + 'template' => '{{#myif foo}}YES{{else}}NO{{/myif}}', + 'data' => Array('foo' => 1), + 'options' => Array( + 'hbhelpers' => Array('myif'), + ), + 'expected' => 'YES', + ), + + Array( + 'template' => '{{#mylogic 0 foo bar}}YES:{{.}}{{else}}NO:{{.}}{{/mylogic}}', + 'data' => Array('foo' => 'FOO', 'bar' => 'BAR'), + 'options' => Array( + 'hbhelpers' => Array('mylogic'), + ), + 'expected' => 'NO:BAR', + ), + + Array( + 'template' => '{{#mylogic true foo bar}}YES:{{.}}{{else}}NO:{{.}}{{/mylogic}}', + 'data' => Array('foo' => 'FOO', 'bar' => 'BAR'), + 'options' => Array( + 'hbhelpers' => Array('mylogic'), + ), + 'expected' => 'YES:FOO', + ), + + Array( + 'template' => '{{#mywith foo}}YA: {{name}}{{/mywith}}', + 'data' => Array('name' => 'OK?', 'foo' => Array('name' => 'OK!')), + 'options' => Array( + 'hbhelpers' => Array('mywith'), + ), + 'expected' => 'YA: OK!', + ), + + Array( + 'template' => '{{mydash \'abc\' "dev"}}', + 'data' => Array('a' => 'a', 'b' => 'b', 'c' => Array('c' => 'c'), 'd' => 'd', 'e' => 'e'), + 'options' => Array( + 'hbhelpers' => Array('mydash'), + ), + 'expected' => 'abc-dev', + ), + + Array( + 'template' => '{{mydash \'a b c\' "d e f"}}', + 'data' => Array('a' => 'a', 'b' => 'b', 'c' => Array('c' => 'c'), 'd' => 'd', 'e' => 'e'), + 'options' => Array( + 'flags' => LightnCandy::FLAG_ADVARNAME, + 'hbhelpers' => Array('mydash'), + ), + 'expected' => 'a b c-d e f', + ), + + Array( + 'template' => '{{mydash "abc" (test_array 1)}}', + 'data' => Array('a' => 'a', 'b' => 'b', 'c' => Array('c' => 'c'), 'd' => 'd', 'e' => 'e'), + 'options' => Array( + 'flags' => LightnCandy::FLAG_ADVARNAME, + 'hbhelpers' => Array('mydash'), + 'helpers' => Array('test_array'), + ), + 'expected' => 'abc-NOT_ARRAY', + ), + + Array( + 'template' => '{{mydash "abc" (myjoin a b)}}', + 'data' => Array('a' => 'a', 'b' => 'b', 'c' => Array('c' => 'c'), 'd' => 'd', 'e' => 'e'), + 'options' => Array( + 'flags' => LightnCandy::FLAG_ADVARNAME, + 'hbhelpers' => Array('mydash', 'myjoin'), + ), + 'expected' => 'abc-ab', + ), + + Array( + 'template' => '{{#with people}}Yes , {{name}}{{else}}No, {{name}}{{/with}}', + 'data' => Array('people' => Array('name' => 'Peter'), 'name' => 'NoOne'), + 'options' => Array('flags' => LightnCandy::FLAG_WITH), + 'expected' => 'Yes , Peter', + ), + + Array( + 'template' => '{{#with people}}Yes , {{name}}{{else}}No, {{name}}{{/with}}', + 'data' => Array('name' => 'NoOne'), + 'options' => Array('flags' => LightnCandy::FLAG_WITH), + 'expected' => 'No, NoOne', + ), + + Array( + 'template' => <<<VAREND +<ul> + <li>1. {{helper1 name}}</li> + <li>2. {{helper1 value}}</li> + <li>3. {{myClass::helper2 name}}</li> + <li>4. {{myClass::helper2 value}}</li> + <li>5. {{he name}}</li> + <li>6. {{he value}}</li> + <li>7. {{h2 name}}</li> + <li>8. {{h2 value}}</li> + <li>9. {{link name}}</li> + <li>10. {{link value}}</li> + <li>11. {{alink url text}}</li> + <li>12. {{{alink url text}}}</li> +</ul> +VAREND + , + 'data' => Array('name' => 'John', 'value' => 10000, 'url' => 'http://yahoo.com', 'text' => 'You&Me!'), + 'options' => Array( + 'flags' => LightnCandy::FLAG_ERROR_LOG | LightnCandy::FLAG_HANDLEBARSJS, + 'helpers' => Array( + 'helper1', + 'myClass::helper2', + 'he' => 'helper1', + 'h2' => 'myClass::helper2', + 'link' => function ($arg) { + return "<a href=\"{$arg}\">click here</a>"; + }, + 'alink', + ) + ), + 'expected' => <<<VAREND +<ul> + <li>1. -Array-</li> + <li>2. -Array-</li> + <li>3. =Array=</li> + <li>4. =Array=</li> + <li>5. -Array-</li> + <li>6. -Array-</li> + <li>7. =Array=</li> + <li>8. =Array=</li> + <li>9. <a href="Array">click here</a></li> + <li>10. <a href="Array">click here</a></li> + <li>11. <a href="Array">Array</a></li> + <li>12. <a href="Array">Array</a></li> +</ul> +VAREND + ), + + Array( + 'template' => '{{test.test}} == {{test.test3}}', + 'data' => Array('test' => new myClass()), + 'options' => Array('flags' => LightnCandy::FLAG_INSTANCE), + 'expected' => "testMethod OK! == -- test3:Array\n(\n)\n", + ), + + Array( + 'template' => '{{test.test}} == {{test.bar}}', + 'data' => Array('test' => new foo()), + 'options' => Array('flags' => LightnCandy::FLAG_INSTANCE), + 'expected' => ' == OK!', + ), + + Array( + 'template' => '{{#each foo}}{{@key}}: {{.}},{{/each}}', + 'data' => Array('foo' => Array(1,'a'=>'b',5)), + 'expected' => ': 1,: b,: 5,', + ), + + Array( + 'template' => '{{#each foo}}{{@key}}: {{.}},{{/each}}', + 'data' => Array('foo' => Array(1,'a'=>'b',5)), + 'options' => Array('flags' => LightnCandy::FLAG_SPVARS), + 'expected' => '0: 1,a: b,1: 5,', + ), + + Array( + 'template' => '{{#each foo}}{{@key}}: {{.}},{{/each}}', + 'data' => Array('foo' => new twoDimensionIterator(2, 3)), + 'options' => Array('flags' => LightnCandy::FLAG_SPVARS), + 'expected' => '0x0: 0,1x0: 0,0x1: 0,1x1: 1,0x2: 0,1x2: 2,', + ), + + Array( + 'template' => " {{#foo}}\n {{name}}\n{{/foo}}\n ", + 'data' => Array('foo' => Array(Array('name' => 'A'),Array('name' => 'd'),Array('name' => 'E'))), + 'options' => Array('flags' => LightnCandy::FLAG_MUSTACHESP), + 'expected' => " A\n d\n E\n ", + ), + + Array( + 'template' => "{{bar}}\n {{#foo}}\n {{name}}\n{{/foo}}\n ", + 'data' => Array('bar' => 'OK', 'foo' => Array(Array('name' => 'A'),Array('name' => 'd'),Array('name' => 'E'))), + 'options' => Array('flags' => LightnCandy::FLAG_MUSTACHESP), + 'expected' => "OK\n A\n d\n E\n ", + ), + + Array( + 'template' => " {{#if foo}}\nYES\n{{else}}\nNO\n{{/if}}\n", + 'data' => null, + 'options' => Array('flags' => LightnCandy::FLAG_MUSTACHESP | LightnCandy::FLAG_ELSE), + 'expected' => "NO\n", + ), + + Array( + 'template' => " {{#each foo}}\n{{@key}}: {{.}}\n{{/each}}\nDONE", + 'data' => Array('foo' => Array('a' => 'A', 'b' => 'BOY!')), + 'options' => Array('flags' => LightnCandy::FLAG_SPVARS | LightnCandy::FLAG_MUSTACHESP), + 'expected' => "a: A\nb: BOY!\nDONE", + ), + + Array( + 'template' => "{{>test1}}\n {{>test1}}\nDONE\n", + 'data' => null, + 'options' => Array( + 'flags' => LightnCandy::FLAG_MUSTACHESP | LightnCandy::FLAG_MUSTACHEPAIN, + 'partials' => Array('test1' => "1:A\n 2:B\n 3:C\n 4:D\n5:E\n"), + ), + 'expected' => "1:A\n 2:B\n 3:C\n 4:D\n5:E\n 1:A\n 2:B\n 3:C\n 4:D\n 5:E\nDONE\n", + ), + + Array( + 'template' => "{{>test1}}\n {{>test1}}\nDONE\n", + 'data' => null, + 'options' => Array( + 'flags' => LightnCandy::FLAG_MUSTACHESP, + 'partials' => Array('test1' => "1:A\n 2:B\n 3:C\n 4:D\n5:E\n"), + ), + 'expected' => "1:A\n 2:B\n 3:C\n 4:D\n5:E\n1:A\n 2:B\n 3:C\n 4:D\n5:E\nDONE\n", + ), + + Array( + 'template' => "{{>test1}}\n {{>test1}}\nDONE\n", + 'data' => null, + 'options' => Array( + 'flags' => LightnCandy::FLAG_MUSTACHESP | LightnCandy::FLAG_RUNTIMEPARTIAL, + 'partials' => Array('test1' => "1:A\n 2:B\n 3:C\n 4:D\n5:E\n"), + ), + 'expected' => "1:A\n 2:B\n 3:C\n 4:D\n5:E\n1:A\n 2:B\n 3:C\n 4:D\n5:E\nDONE\n", + ), + + Array( + 'template' => "ST:\n{{#foo}}\n {{>test1}}\n{{/foo}}\nOK\n", + 'data' => Array('foo' => Array(1, 2)), + 'options' => Array( + 'flags' => LightnCandy::FLAG_MUSTACHESP | LightnCandy::FLAG_MUSTACHEPAIN | LightnCandy::FLAG_HANDLEBARSJS, + 'partials' => Array('test1' => "1:A\n 2:B({{@index}})\n"), + ), + 'expected' => "ST:\n 1:A\n 2:B(0)\n 1:A\n 2:B(1)\nOK\n", + ), + + Array( + 'template' => "A\n {{#if 1}} \n\na\n{{#with 2}}\n123\n\n{{/with}}\n{{/if}} \n \n\n456", + 'data' => null, + 'options' => Array('flags' => LightnCandy::FLAG_WITH | LightnCandy::FLAG_MUSTACHESP), + 'expected' => "A\n\na\n123\n\n \n\n456", + ), + + Array( + 'template' => "\n{{#with 1}}\n\n{{#with 1}}\nb\n\n{{/with}}\n{{/with}}\nC", + 'data' => null, + 'options' => Array('flags' => LightnCandy::FLAG_WITH | LightnCandy::FLAG_MUSTACHESP), + 'expected' => "\n\nb\n\nC", + ), + + Array( + 'template' => ">{{helper1 \"===\"}}<", + 'data' => null, + 'options' => Array( + 'flags' => LightnCandy::FLAG_HANDLEBARSJS, + 'hbhelpers' => Array( + 'helper1', + ) + ), + 'expected' => ">-===-<", + ), + + Array( + 'template' => "{{foo}}", + 'data' => Array('foo' => 'A&B " \''), + 'options' => Array('flags' => LightnCandy::FLAG_NOESCAPE), + 'expected' => "A&B \" '", + ), + + Array( + 'template' => "{{foo}}", + 'data' => Array('foo' => 'A&B " \''), + 'options' => null, + 'expected' => "A&B " '", + ), + ); + + return array_map(function($i) { + if (!isset($i['debug'])) { + $i['debug'] = 0; + } + return Array($i); + }, $issues); + } +} + +?> diff --git a/vendor/zordius/lightncandy/tests/test1.tmpl b/vendor/zordius/lightncandy/tests/test1.tmpl new file mode 100644 index 00000000..190a1803 --- /dev/null +++ b/vendor/zordius/lightncandy/tests/test1.tmpl @@ -0,0 +1 @@ +123 diff --git a/vendor/zordius/lightncandy/tests/test2.tmpl b/vendor/zordius/lightncandy/tests/test2.tmpl new file mode 100644 index 00000000..df980f7e --- /dev/null +++ b/vendor/zordius/lightncandy/tests/test2.tmpl @@ -0,0 +1 @@ +a{{> test1}}b diff --git a/vendor/zordius/lightncandy/tests/test3.tmpl b/vendor/zordius/lightncandy/tests/test3.tmpl new file mode 100644 index 00000000..0b4e6bb9 --- /dev/null +++ b/vendor/zordius/lightncandy/tests/test3.tmpl @@ -0,0 +1 @@ +New context:{{.}} |