X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/vendor/zordius/lightncandy/src/lightncandy.php diff --git a/vendor/zordius/lightncandy/src/lightncandy.php b/vendor/zordius/lightncandy/src/lightncandy.php new file mode 100644 index 00000000..b7127b43 --- /dev/null +++ b/vendor/zordius/lightncandy/src/lightncandy.php @@ -0,0 +1,2779 @@ + + */ + +/** + * 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_BARE = 33554432; + 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; + const FLAG_RAWBLOCK = 134217728; + + // 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; + + // 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 = 6684672; // FLAG_ERROR_SKIPPARTIAL + FLAG_MUSTACHESP + FLAG_MUSTACHELOOKUP + FLAG_MUSTACHEPAIN + const FLAG_HANDLEBARS = 161619936; // FLAG_THIS + FLAG_WITH + FLAG_PARENT + FLAG_JSQUOTE + FLAG_ADVARNAME + FLAG_SPACECTL + FLAG_NAMEDARG + FLAG_SPVARS + FLAG_SLASH + FLAG_ELSE + FLAG_RAWBLOCK + FLAG_MUSTACHESP + FLAG_MUSTACHEPAIN + const FLAG_HANDLEBARSJS = 161619960; // FLAG_JS + FLAG_HANDLEBARS + const FLAG_INSTANCE = 98304; // FLAG_PROPERTY + FLAG_METHOD + + // RegExps + const VARNAME_SEARCH = '/(\\[[^\\]]+\\]|[^\\[\\]\\.]+)/'; + const EXTENDED_COMMENT_SEARCH = '/{{!--.*?--}}/s'; + const IS_SUBEXP_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; + public static $TMP_JS_FUNCTION_STR = "!!\aFuNcTiOn\a!!"; + + /** + * Compile handlebars template into PHP code. + * + * @param string $template handlebars template string + * @param array $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; + } + + $template = static::stripExtendedComments($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; + } + + $context['scan'] = false; + + // Do PHP code generation. + static::setupToken($context); + + // Handle dynamic partials + static::handleDynamicPartial($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); + } + + /** + * Strip extended comments {{!-- .... --}} + * + * @param string $template handlebars template string + * + * @return string Stripped template + * + * @expect 'abc' when input 'abc' + * @expect 'abc{{!}}cde' when input 'abc{{!}}cde' + * @expect 'abc{{! }}cde' when input 'abc{{!----}}cde' + */ + protected static function stripExtendedComments($template) { + return preg_replace(static::EXTENDED_COMMENT_SEARCH, '{{! }}', $template); + } + + /** + * Include all partials when using dynamic partials + */ + protected static function handleDynamicPartial(&$context) { + if ($context['usedFeature']['dynpartial'] == 0) { + return; + } + + foreach ($context['partials'] as $name => $code) { + static::readPartial($name, $context); + } + } + + /** + * 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 $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 === '}}')) { + if ($context['flags']['rawblock']) { + $left = '\\{{2,4}'; + $right = '\\}{2,4}'; + } else { + $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 $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 $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 $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']); + $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; + $phpstart = $context['flags']['bare'] ? '' : '"; + + // Return generated PHP code string. + return "{$phpstart}return function (\$in, \$debugopt = $debug) { + \$cx = array( + 'flags' => array( + 'jstrue' => $flagJStrue, + 'jsobj' => $flagJSObj, + 'spvar' => $flagSPVar, + 'prop' => $flagProp, + 'method' => $flagMethod, + 'mustlok' => $flagMustlok, + 'echo' => $flagEcho, + 'debug' => \$debugopt, + ), + 'constants' => $constants, + 'helpers' => $helpers, + 'blockhelpers' => $bhelpers, + 'hbhelpers' => $hbhelpers, + 'partials' => array({$context['partialCode']}), + 'scopes' => array(), + 'sp_vars' => array('root' => \$in), + 'lcrun' => '{$context['lcrun']}', +$libstr + ); + {$context['renderex']} + {$context['ops']['op_start']}'$code'{$context['ops']['op_end']} +}$phpend"; + } + + /** + * Build context from options + * + * @param array $options input options + * + * @return array 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, + 'bare' => $flags & self::FLAG_BARE, + '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, + 'debug' => $flags & self::FLAG_RENDER_DEBUG, + 'prop' => $flags & self::FLAG_PROPERTY, + 'method' => $flags & self::FLAG_METHOD, + 'runpart' => $flags & self::FLAG_RUNTIMEPARTIAL, + 'rawblock' => $flags & self::FLAG_RAWBLOCK, + ), + 'level' => 0, + 'scan' => true, + '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, + 'comment' => 0, + 'partial' => 0, + 'dynpartial' => 0, + 'helper' => 0, + 'bhelper' => 0, + 'hbhelper' => 0, + 'delimiter' => 0, + 'subexp' => 0, + 'rawblock' => 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'] : '', + 'prepartial' => (isset($options['prepartial']) && is_callable($options['prepartial'])) ? $options['prepartial'] : false, + 'lcrun' => isset($options['lcrun']) ? $options['lcrun'] : 'LCRun3', + 'rawblock' => false, + ); + + $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 $context prepared context + * @param array $options input options + * @param string $tname helper table name + * + * @return array 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 $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 (preg_match(static::IS_SUBEXP_SEARCH, $name)) { + if ($context['flags']['runpart']) { + $context['usedFeature']['dynpartial']++; + return; + } else { + $context['error'][] = "You use dynamic partial name as '$name', this only works with option FLAG_RUNTIMEPARTIAL enabled"; + return; + } + } + + if (!$context['flags']['skippartial']) { + $context['error'][] = "Can not find partial file for '$name', you should set correct basedir and fileext in options"; + } + } + + /** + * preprocess partial template before it be stored into context + * + * @param string $tmpl partial template + * @param string $name partial name + * @param array $context Current context of compiler progress. + * + * @return string|null $content processed partial template + * + * @expect 'hey' when input 'hey', 'haha', Array('prepartial' => false) + * @expect 'haha-hoho' when input 'hoho', 'haha', Array('prepartial' => function ($tmpl, $name) {return "$name-$tmpl";}) + */ + protected static function prePartial($tmpl, &$name, &$context) { + return $context['prepartial'] ? $context['prepartial']($tmpl, $name, $context) : $tmpl; + } + + /** + * locate partial file, return the file name + * + * @param string $name partial name + * @param array $context Current context of compiler progress. + * + * @return string|null $content partial content + */ + protected static function resolvePartial(&$name, &$context) { + if (isset($context['partials'][$name])) { + return static::prePartial($context['partials'][$name], $name, $context); + } + + foreach ($context['basedir'] as $dir) { + foreach ($context['fileext'] as $ext) { + $fn = "$dir/$name$ext"; + if (file_exists($fn)) { + return static::prePartial(file_get_contents($fn), $name, $context); + } + } + } + return null; + } + + /** + * compile partial file, stored in context + * + * @param string $name partial name + * @param array $context Current context of compiler progress. + * @param string $content partial content + */ + protected static function compilePartial(&$name, &$context, $content) { + $context['usedPartial'][$name] = static::escapeTemplate(static::stripExtendedComments($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, str_replace('function', self::$TMP_JS_FUNCTION_STR, $context['usedPartial'][$name]), $name); + if ($context['flags']['mustpi']) { + $sp = ', $sp'; + $code = preg_replace('/^/m', "'{$context['ops']['seperator']}\$sp{$context['ops']['seperator']}'", $code); + // callbacks inside partial should be aware of $sp + $code = str_replace(self::$TMP_JS_FUNCTION_STR, 'function', preg_replace('/\bfunction\s*\((.*?)\)\s*{/', 'function(\\1)use($sp){', $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 $options current compile option + * + * @return array 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 $options current compile option + * + * @return array 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 $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 $context current compile context + * + * @return string + */ + protected static function exportLCRun($context) { + if ($context['flags']['standalone'] == 0) { + return ''; + } + + $class = new ReflectionClass($context['lcrun']); + $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 $context current compile context + * + * @return string + */ + protected static function exportLCRunConstant($context) { + if ($context['flags']['standalone'] == 0) { + return 'array()'; + } + + $class = new ReflectionClass($context['lcrun']); + $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 $context current compile context + * @param string $code PHP code string of the method + * + * @return 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 $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(), 'rawblock' => 0) + * @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 ' . ($context['rawblock'] ? "{{{{{$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 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. + * @param boolean $delete Optional, delete temp php file when set to tru. Default is true, set it to false for debug propose + * + * @return Closure|false result of include() + * + * @deprecated + */ + public static function prepare($php, $tmpDir = null, $delete = true) { + if (!ini_get('allow_url_include') || !ini_get('allow_url_fopen')) { + if (!is_string($tmpDir) || !is_dir($tmpDir)) { + $tmpDir = sys_get_temp_dir(); + } + } + + if (is_dir($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; + } + + $phpfunc = include($fn); + + if ($delete) { + unlink($fn); + } + + return $phpfunc; + } + + return include('data://text/plain,' . urlencode($php)); + } + + /** + * Internal method used by compile(). Get function name for standalone or none standalone template. + * + * @param array $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), 'lcrun' => 'LCRun3'), 'test', '' + * @expect 'LCRun3::test2(' when input array('flags' => array('standalone' => 0, 'debug' => 0), 'lcrun' => 'LCRun3'), 'test2', '' + * @expect "\$cx['funcs']['test3'](" when input array('flags' => array('standalone' => 1, 'debug' => 0), 'lcrun' => 'LCRun3'), 'test3', '' + * @expect 'LCRun3::debug(\'abc\', \'test\', ' when input array('flags' => array('standalone' => 0, 'debug' => 1), 'lcrun' => 'LCRun3'), '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" : "{$context['lcrun']}::$name($dbg"; + } + + /** + * Internal method used by getArrayCode(). Get variable names translated string. + * + * @param array $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 $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 $vn variable name array. + * @param array $context current compile context + * + * @return 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) { + $vars = array(array(), array()); + $exps = array(); + foreach ($vn as $i => $v) { + $V = static::getVariableNameOrSubExpression($v, $context); + 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 $context current compile context + * @param boolean $keepCount keep original usage count + * + * @return array code representing passed expression + */ + protected static function compileSubExpression($subExpression, &$context, $keepCount = false) { + // 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); + $oldCount = $context['usedFeature']; + 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; + + if ($keepCount) { + $context['usedFeature'] = $oldCount; + } else { + $context['usedFeature']['subexp']++; + // detect handlebars custom helpers. + if (isset($context['hbhelpers'][$vars[0][0]])) { + $context['usedFeature']['hbhelper']++; + } else { + // detect custom helpers. + if (isset($context['helpers'][$vars[0][0]])) { + $context['usedFeature']['helper']++; + } + } + } + + return array($ret ? $ret : '', $subExpression); + } + + /** + * Internal method used by compile(). + * + * @param array $var variable parsed path + * @param array $context current compile context + * + * @return array variable names + */ + protected static function getVariableNameOrSubExpression($var, &$context) { + if (isset($var[0]) && preg_match(static::IS_SUBEXP_SEARCH, $var[0])) { + return static::compileSubExpression($var[0], $context, true); + } + return static::getVariableName($var, $context); + } + + /** + * Internal method used by compile(). + * + * @param array $var variable parsed path + * @param array $context current compile context + * + * @return array variable names + * + * @expect array('$in', 'this') when input array(null), array('flags'=>array('spvar'=>true,'debug'=>0)) + * @expect array('((isset($in[\'true\']) && is_array($in)) ? $in[\'true\'] : null)', '[true]') when input array('true'), array('flags'=>array('spvar'=>true,'debug'=>0,'prop'=>0,'method'=>0,'mustlok'=>0)) + * @expect array('((isset($in[\'false\']) && is_array($in)) ? $in[\'false\'] : null)', '[false]') when input array('false'), array('flags'=>array('spvar'=>true,'debug'=>0,'prop'=>0,'method'=>0,'mustlok'=>0)) + * @expect array('true', 'true') when input array(0, 'true'), array('flags'=>array('spvar'=>true,'debug'=>0)) + * @expect array('false', 'false') when input array(0, 'false'), array('flags'=>array('spvar'=>true,'debug'=>0)) + * @expect array('((isset($in[\'2\']) && is_array($in)) ? $in[\'2\'] : null)', '[2]') when input array('2'), array('flags'=>array('spvar'=>true,'debug'=>0,'prop'=>0,'method'=>0,'mustlok'=>0)) + * @expect array('2', '2') when input array(0, '2'), array('flags'=>array('spvar'=>true,'debug'=>0,'prop'=>0,'method'=>0)) + * @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,'prop'=>0,'method'=>0,'mustlok'=>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,'prop'=>0,'method'=>0,'mustlok'=>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,'prop'=>0,'method'=>0,'mustlok'=>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,'prop'=>0,'method'=>0,'mustlok'=>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('"a"', '"a"') when input array(0, '"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), 'lcrun' => 'LCRun3') + */ + protected static function getVariableName($var, &$context) { + if (isset($var[0]) && ($var[0] === 0)) { + return array($var[1], preg_replace('/\'(.*)\'/', '$1', $var[1])); + } + + $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); + } + } + + if (isset($var[0])) { + // 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 $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 $context Current compile content. + * + * @return array Return variable name array + * + * @expect array('this') when input 'this', array('flags' => array('advar' => 0, 'this' => 0)) + * @expect array() when input 'this', array('flags' => array('advar' => 0, 'this' => 1)) + * @expect array(1) when input '../', array('flags' => array('advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0), 'scan' => true) + * @expect array(1) when input '../.', array('flags' => array('advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0), 'scan' => true) + * @expect array(1) when input '../this', array('flags' => array('advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0), 'scan' => true) + * @expect array(1, 'a') when input '../a', array('flags' => array('advar' => 0, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0), 'scan' => true) + * @expect array(2, 'a', 'b') when input '../../a.b', array('flags' => array('advar' => 0, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0), 'scan' => true) + * @expect array(2, '[a]', 'b') when input '../../[a].b', array('flags' => array('advar' => 0, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0), 'scan' => true) + * @expect array(2, 'a', 'b') when input '../../[a].b', array('flags' => array('advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0), 'scan' => true) + * @expect array('id') when input 'this.id', array('flags' => array('advar' => 1, 'this' => 1, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + * @expect array(0, '\'a.b\'') when input '"a.b"', array('flags' => array('advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + * @expect array(0, '123') when input '123', array('flags' => array('advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + * @expect array(0, 'null') when input 'null', array('flags' => array('advar' => 1, 'this' => 0, 'parent' => 1), 'usedFeature' => array('parent' => 0)) + */ + protected static function fixVariable($v, &$context) { + // handle number + if (is_numeric($v)) { + // convert 0x00 or 0b00 numbers to decimal + return array(0, (string) 1 * $v); + } + + // handle double quoted string + if (preg_match('/^"(.*)"$/', $v, $matched)) { + return array(0, "'" . preg_replace('/([^\\\\])\\\\\\\\"/', '$1"', preg_replace('/^\\\\\\\\"/', '"', $matched[1])) . "'"); + } + + // handle single quoted string + if (preg_match('/^\\\\\'(.*)\\\\\'$/', $v, $matched)) { + return array(0, "'$matched[1]'"); + } + + // handle boolean, null and undefined + if (preg_match('/^(true|false|null|undefined)$/', $v)) { + return array(0, ($v === 'undefined') ? 'null' : $v); + } + + $ret = array(); + $levels = 0; + + // 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'] += ($context['scan'] ? 1 : 0); + } + + if ($context['flags']['advar'] && preg_match('/\\]/', $v)) { + preg_match_all(self::VARNAME_SEARCH, $v, $matchedall); + } else { + preg_match_all('/([^\\.\\/]+)/', $v, $matchedall); + } + + foreach ($matchedall[1] as $m) { + if ($context['flags']['advar'] && substr($m, 0, 1) === '[') { + $ret[] = substr($m, 1, -1); + } else if ((!$context['flags']['this'] || ($m !== 'this')) && ($m !== '.')) { + $ret[] = $m; + } + } + + return $ret; + } + + /** + * Internal method used by scanFeatures() and compile(). Parse the token and return parsed result. + * + * @param array $token preg_match results + * @param array $context current compile context + * + * @return array Return parsed result + * + * @expect array(false, array(array())) when input array(0,0,0,0,0,0,''), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'scan' => false, 'rawblock' => false) + * @expect array(true, array(array())) when input array(0,0,0,'{{{',0,0,''), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 0), 'scan' => false, 'rawblock' => false) + * @expect array(true, array(array())) when input array(0,0,0,0,0,0,''), array('flags' => array('advar' => 0, 'this' => 1, 'namev' => 0, 'noesc' => 1), 'scan' => false, 'rawblock' => false) + * @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), 'scan' => false, 'rawblock' => false) + * @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), 'scan' => false, 'rawblock' => false) + * @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), 'scan' => false, 'rawblock' => false) + * @expect array(false, array(array('a'), array(0, '\'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), 'scan' => false, 'rawblock' => false) + * @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), 'scan' => false, 'rawblock' => false) + * @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), 'scan' => false, 'rawblock' => false) + * @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), 'scan' => false, 'rawblock' => false) + * @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), 'scan' => false, 'rawblock' => false) + * @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), 'scan' => false, 'rawblock' => false) + * @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), 'scan' => false, 'rawblock' => false) + * @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), 'scan' => false, 'rawblock' => false) + * @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), 'scan' => false, 'rawblock' => false) + * @expect array(false, array(array('a'), 'q' => array(0, '\'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), 'scan' => false, 'rawblock' => false) + * @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, 'exhlp' => 1), 'ops' => array('seperator' => ''), 'usedFeature' => array('subexp' => 0), 'scan' => false, 'rawblock' => false) + * @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, 'noesc' => 0, 'this' => 0), 'scan' => false, 'rawblock' => false) + * @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, 'exhlp' => 1), 'ops' => array('seperator' => ''), 'usedFeature' => array('subexp' => 0), 'scan' => false, 'rawblock' => false) + * @expect array(false, array(array('a'), array(0, '\' 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), 'scan' => false, 'rawblock' => false) + * @expect array(false, array(array('a'), 'q' => array(0, '\' 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), 'scan' => false, 'rawblock' => false) + * @expect array(false, array(array('foo'), array(0, "' =='"), array('bar'))) when input array(0,0,0,0,0,0,"foo \' ==\' bar"), array('flags' => array('advar' => 1, 'namev' => 1, 'noesc' => 0, 'this' => 0), 'scan' => false, 'rawblock' => false) + * @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), 'scan' => false, 'rawblock' => false) + * @expect array(false, array(array('a'), 'q' => array(0, "' d e'"))) when input array(0,0,0,0,0,0,"a q=\' d e\'"), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0), 'scan' => false, 'rawblock' => false) + * @expect array(false, array('q' => array('( foo bar)'))) when input array(0,0,0,0,0,0,'q=( foo bar)'), array('flags' => array('advar' => 1, 'this' => 1, 'namev' => 1, 'noesc' => 0, 'exhlp' => 0), 'scan' => false, 'usedFeature' => array(), 'ops' => array('seperator' => 0), 'rawblock' => false) + */ + protected static function parseTokenArgs(&$token, &$context) { + $inner = $token[self::POS_INNERTAG]; + trim($inner); + + // skip parse when inside raw block + if ($context['rawblock'] && !(($token[self::POS_BEGINTAG] === '{{{{') && ($token[self::POS_OP] === '/') && ($context['rawblock'] === $inner))) { + return array(-1, $token); + } + + $token[self::POS_INNERTAG] = $inner; + + // 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()); + } + + // Handle raw block + if ($token[self::POS_BEGINTAG] === '{{{{') { + if ($token[self::POS_ENDTAG] !== '}}}}') { + $context['error'][] = 'Bad token ' . static::tokenString($token) . ' ! Do you mean {{{{' . static::tokenString($token, 4) . '}}}} ?'; + } + if ($context['rawblock']) { + static::setupToken($context); + $context['rawblock'] = false; + } else { + if ($token[self::POS_OP]) { + $context['error'][] = "Wrong raw block begin with " . static::tokenString($token) . ' ! Remove "' . $token[self::POS_OP] . '" to fix this issue.'; + } + static::setupToken($context, '{{{{', '}}}}'); + $token[self::POS_OP] = '#'; + $context['rawblock'] = $token[self::POS_INNERTAG]; + } + $token[self::POS_BEGINTAG] = '{{'; + $token[self::POS_ENDTAG] = '}}'; + } + + // Skip validation on comments + if ($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 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; + } + + // 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(static::IS_SUBEXP_SEARCH, $var)) { + static::compileSubExpression($var, $context, !$context['scan']); + $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]; + // Compile subexpressions for named arguments + if (preg_match(static::IS_SUBEXP_SEARCH, $var)) { + static::compileSubExpression($var, $context, !$context['scan']); + $ret[$idx] = array($var); + continue; + } + } + } + + $esc = $context['scan'] ? '' : '\\\\'; + if ($context['flags']['advar'] && !preg_match("/^(\"|$esc')(.*)(\"|$esc')$/", $var)) { + // 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) . ' !'; + } else { + if (!$context['scan']) { + $name = preg_replace('/(\\[.+?\\])/', '', $var); + // Scan for invalid charactors which not be protected by [ ] + // now make ( and ) pass, later fix + if (preg_match('/[!"#%\'*+,;<=>{|}~]/', $name)) { + $context['error'][] = "Wrong variable naming as '$var' in " . static::tokenString($token) . ' ! You should wrap ! " # % & \' * + , ; < = > { | } ~ into [ ]'; + } + } + } + } + + if (($idx === 0) && ($token[self::POS_OP] === '>')) { + $var = array(preg_replace('/^("(.+)")|(\\[(.+)\\])$/', '$2$4', $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'] || $context['rawblock'], $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 $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 $context current compile context + * @param 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 (isset($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']++; + + if (!isset($vars[0][0])) { + return; + } + + if (is_string($vars[0][0])) { + // 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 $context current compile context + */ + protected static function scanFeatures($token, &$context) { + list($raw, $vars) = static::parseTokenArgs($token, $context); + + if ($raw === -1) { + return; + } + + 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']++; + } + + foreach ($vars as $var) { + if (!isset($var[0])) { + if ($context['level'] == 0) { + $context['usedFeature']['rootthis']++; + } + $context['usedFeature']['this']++; + } + } + + if (!isset($vars[0][0])) { + return; + } + + if ($vars[0][0] === 'else') { + if ($context['flags']['else']) { + return $context['usedFeature']['else']++; + } + } + + // 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 $token detected handlebars {{ }} token + * @param array $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 $token detected handlebars {{ }} token + * @param array $vars parsed arguments list + * @param array $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 $token detected handlebars {{ }} token + * @param array $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); + + // Do not touch the tag, keep it as is. + if ($raw === -1) { + return ".'" . static::tokenString($token) . "'."; + } + + $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 (isset($vars[0][0])) { + 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 $token detected handlebars {{ }} token + * @param array $context current compile context + * @param array $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 (($context['usedFeature']['dynpartial'] === 0) && !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); + $tag = ">$p[0] " .implode(' ', $v[1]); + if ($context['flags']['runpart']) { + if (preg_match(static::IS_SUBEXP_SEARCH, $p[0])) { + list($p) = static::compileSubExpression($p[0], $context); + } else { + $p = "'$p[0]'"; + } + $sp = $context['tokens']['partialind'] ? ", '{$context['tokens']['partialind']}'" : ''; + return $context['ops']['seperator'] . static::getFuncName($context, 'p', $tag) . "\$cx, $p, $v[0]$sp){$context['ops']['seperator']}"; + } + 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 (!isset($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 !== null) { + 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 !== null) { + 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 $context current compile context + * @param array $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) { + if (!isset($vars[0][0])) { + return; + } + $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); + 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 $token detected handlebars {{ }} token + * @param array $context current compile context + * @param array $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 $context current compile context + * @param array $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::getVariableNameOrSubExpression($vars[1], $context) : array(null, array()); + switch (isset($vars[0][0]) ? $vars[0][0] : null) { + 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::getVariableNameOrSubExpression($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 $context current compile context + * @param array $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) { + if (!$context['flags']['exhlp']) { + $context['error'][] = "Can not find custom helper function defination {$vars[0][0]}() !"; + } + } + return; + } + + $fn = $raw ? 'raw' : $context['ops']['enc']; + $ch = array_shift($vars); + $v = static::getVariableNames($vars, $context); + 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 $context current compile context + * @param array $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']}"; + default: + } + } + $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 $context current compile context + * @param array $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 $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 $cx render time context + * + * @expect '{{123}}' when input '123', 'miss', array('flags' => array('debug' => LCRun3::DEBUG_TAGS), 'lcrun' => 'LCRun3'), '' + * @expect '{{#123}}{{/123}}' when input '123', 'wi', array('flags' => array('debug' => LCRun3::DEBUG_TAGS_HTML), 'lcrun' => 'LCRun3'), 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] : "{$cx['lcrun']}::$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 !== '') ? '' : '') : '') + . ($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 = ''; + } + } + 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 $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 $cx render time context + * @param array $base current variable context + * @param array $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--; + switch ($count) { + case -1: + $base = $cx['sp_vars']['root']; + break; + case -2: + return null; + default: + $base = $cx['scopes'][$count]; + } + } + } + + /** + * LightnCandy runtime method for {{#if var}}. + * + * @param array $cx render time context + * @param array|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 !== 0.0) && ($v !== '') && (is_array($v) ? (count($v) > 0) : true); + } + + /** + * LightnCandy runtime method for {{#if var}} when {{../var}} used. + * + * @param array $cx render time context + * @param array|string|integer|null $v value to be tested + * @param array $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 $cx render time context + * @param array|string|integer|null $var value be tested + * @param array|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 $cx render time context + * @param array|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 $cx render time context + * @param array|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); + } + } else { + return 'Array'; + } + } + + return "$v"; + } + + /** + * LightnCandy runtime method for {{var}} . + * + * @param array $cx render time context + * @param array|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 $cx render time context + * @param array|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 $cx render time context + * @param array|string|integer|null $v value for the section + * @param array|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)), 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)), 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), 'sp_vars'=>array('root' => 0)), array(1,3,4), 0, false, function ($c, $i) {return $i * 2;} + * @expect '038' when input array('flags' => array('spvar' => 1), 'sp_vars'=>array('root' => 0)), 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) || ($v instanceof ArrayObject); + $isTrav = $v instanceof Traversable; + $loop = $each; + $keys = null; + $last = null; + $isObj = false; + + if ($isAry && $else !== null && count($v) === 0) { + $cx['scopes'][] = $in; + $ret = $else($cx, $in); + array_pop($cx['scopes']); + return $ret; + } + + // #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) { + $cx['scopes'][] = $in; + $ret = $cb($cx, $v); + 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) { + $cx['scopes'][] = $in; + $ret = $else($cx, $in); + array_pop($cx['scopes']); + return $ret; + } + + return ''; + } + + /** + * LightnCandy runtime method for {{#with var}} . + * + * @param array $cx render time context + * @param array|string|integer|null $v value to be the new context + * @param array|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 '{"a":"b"}' when input array(), array('a'=>'b'), array('a'=>'c'), function ($c, $i) {return json_encode($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 $cx render time context + * @param string $p partial name + * @param array|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 = '') { + $param = $v[0][0]; + + if (is_array($v[1])) { + if (is_array($v[0][0])) { + $param = array_merge($v[0][0], $v[1]); + } else if (($cx['flags']['method'] || $cx['flags']['prop']) && is_object($v[0][0])) { + foreach ($v[1] as $i => $v) { + $param->$i = $v; + } + } + } + + return call_user_func($cx['partials'][$p], $cx, $param, $sp); + } + + /** + * LightnCandy runtime method for custom helpers. + * + * @param array $cx render time context + * @param string $ch the name of custom helper to be executed + * @param 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 $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 $cx render time context + * @param string $ch the name of custom helper to be executed + * @param array|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|null $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 = null, $else = null) { + $isBlock = (is_object($cb) && ($cb instanceof Closure)); + $args = $vars[0]; + $options = array( + 'name' => $ch, + 'hash' => $vars[1], + '_this' => $isBlock ? $op : $inverted, + ); + + // $invert the logic + if ($inverted) { + $tmp = $else; + $else = $cb; + $cb = $tmp; + } + + if ($isBlock) { + $options['fn'] = function ($context = '_NO_INPUT_HERE_', $data = null) use ($cx, $op, $cb) { + if ($cx['flags']['echo']) { + ob_start(); + } + $cx['scopes'][] = $op; + if ($data) { + $tmp_data = $cx['sp_vars']; + $cx['sp_vars'] = array_merge($cx['sp_vars'], $data['data']); + } + if ($context === '_NO_INPUT_HERE_') { + $ret = $cb($cx, $op); + } else { + $ret = $cb($cx, $context); + } + array_pop($cx['scopes']); + if ($data) { + $cx['sp_vars'] = $tmp_data; + } + return $cx['flags']['echo'] ? ob_get_clean() : $ret; + }; + } + + if ($else) { + $options['inverse'] = function ($context = '_NO_INPUT_HERE_', $data = null) 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']; + } + + $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($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 $cx render time context + * @param string $ch the name of custom helper to be executed + * @param array|string|integer|null $vars variables for the helper + * @param array|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 = null) { + $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; + } +}