$val) * %uniq : A random string that identifies a specific loop or condition. */ protected static $_directives = [ 'if' => ['if (%s):', 'endif;'], 'unless' => ['if (!(%s)):', 'endif;'], 'for' => ['for (%s):', 'endfor;'], 'while' => ['while (%s):', 'endwhile;'], 'switch' => ['switch (%s):', 'endswitch;'], 'foreach' => [ '$__tmp_%uniq = %array ?? []; $__loop_%uniq = $this->_v2_initLoopVar("%uniq", $__tmp_%uniq); foreach ($__tmp_%uniq as %remainder):', '$this->_v2_incrLoopVar($__loop_%uniq); endforeach; $this->_v2_removeLoopVar($__loop_%uniq); unset($__loop_%uniq);', ], 'forelse' => [ '$__tmp_%uniq = %array ?? []; if($__tmp_%uniq): $__loop_%uniq = $this->_v2_initLoopVar("%uniq", $__tmp_%uniq); foreach ($__tmp_%uniq as %remainder):', '$this->_v2_incrLoopVar($__loop_%uniq); endforeach; $this->_v2_removeLoopVar($__loop_%uniq); unset($__loop_%uniq); else:', 'endif;', ], 'once' => [ "if (!isset(\$GLOBALS['tplv2_once']['%uniq'])):", "\$GLOBALS['tplv2_once']['%uniq'] = true; endif;", ], 'fragment' => [ 'ob_start(); $__last_fragment_name = %s;', '$this->_fragments[$__last_fragment_name] = ob_get_flush();', ], 'error' => [ 'if ($this->_v2_errorExists(%s)):', 'endif;', ], 'push' => [ 'ob_start(); if (!isset(self::$_stacks[%s])): self::$_stacks[%s] = []; endif;', 'array_push(self::$_stacks[%s], trim(ob_get_clean()));', ], 'pushif' => [ 'list($__stack_cond, $__stack_name) = [%s]; if ($__stack_cond): ob_start(); if (!isset(self::$_stacks[$__stack_name])): self::$_stacks[$__stack_name] = []; endif;', 'array_push(self::$_stacks[$__stack_name], trim(ob_get_clean())); endif;', ], 'pushonce' => [ 'ob_start(); if (!isset(self::$_stacks[%s])): self::$_stacks[%s] = []; endif;', '$__tmp_%uniq = trim(ob_get_clean()); if (!in_array($__tmp_%uniq, self::$_stacks[%s])): array_push(self::$_stacks[%s], $__tmp_%uniq); endif;', ], 'prepend' => [ 'ob_start(); if (!isset(self::$_stacks[%s])): self::$_stacks[%s] = []; endif;', 'array_unshift(self::$_stacks[%s], trim(ob_get_clean()));', ], 'prependif' => [ 'list($__stack_cond, $__stack_name) = [%s]; if ($__stack_cond): ob_start(); if (!isset(self::$_stacks[$__stack_name])): self::$_stacks[$__stack_name] = []; endif;', 'array_unshift(self::$_stacks[$__stack_name], trim(ob_get_clean())); endif;', ], 'prependonce' => [ 'ob_start(); if (!isset(self::$_stacks[%s])): self::$_stacks[%s] = []; endif;', '$__tmp_%uniq = trim(ob_get_clean()); if (!in_array($__tmp_%uniq, self::$_stacks[%s])): array_unshift(self::$_stacks[%s], $__tmp_%uniq); endif;', ], 'isset' => ['if (isset(%s)):', 'endif;'], 'unset' => ['if (!isset(%s)):', 'endif;'], 'empty' => ['if (empty(%s)):', 'endif;'], 'admin' => ['if ($this->user->isAdmin()):', 'endif;'], 'auth' => ['if ($this->_v2_checkAuth(%s)):', 'endif;'], 'can' => ['if ($this->_v2_checkCapability(1, %s)):', 'endif;'], 'cannot' => ['if ($this->_v2_checkCapability(2, %s)):', 'endif;'], 'canany' => ['if ($this->_v2_checkCapability(3, %s)):', 'endif;'], 'guest' => ['if (!$this->user->isMember()):', 'endif;'], 'desktop' => ['if (!$this->_v2_isMobile()):', 'endif;'], 'mobile' => ['if ($this->_v2_isMobile()):', 'endif;'], 'env' => ['if (!empty($_ENV[%s])):', 'endif;'], 'else' => ['else:'], 'elseif' => ['elseif (%s):'], 'case' => ['case %s:'], 'default' => ['default:'], 'continue' => ['continue;'], 'break' => ['break;'], ]; /** * Cache the compiled regexp for directives here. */ protected static $_directives_regexp; /** * Convert template code into PHP. * * @param string $content * @param Template $template * @return string */ public function convert(string $content, Template $template): string { // Store template info in instance property. $this->template = $template; // Preprocessing. $content = $this->_preprocess($content); // Apply conversions. $content = $this->_addContextSwitches($content); $content = $this->_removeComments($content); $content = $this->_convertVerbatimSections($content); $content = $this->_convertRelativePaths($content); $content = $this->_convertPHPSections($content); $content = $this->_convertFragments($content); $content = $this->_convertClassAliases($content); $content = $this->_convertIncludes($content); $content = $this->_convertResource($content); $content = $this->_convertLoopDirectives($content); $content = $this->_convertInlineDirectives($content); $content = $this->_convertMiscDirectives($content); $content = $this->_convertEchoStatements($content); $content = $this->_addDeprecationMessages($content); // Postprocessing. $content = $this->_postprocess($content); return $content; } /** * Preprocessing. * * @param string $content * @return string */ protected function _preprocess(string $content): string { // Remove trailing whitespace. $content = preg_replace('#[\x20\x09]+$#m', '', $content); return $content; } /** * Insert context switch points (HTML <-> JS). * * @param string $content * @return string */ protected function _addContextSwitches(string $content): string { // Inline styles. $content = preg_replace_callback('#(?<=\s)(style=")([^"]*?)"#i', function($match) { return $match[1] . 'config->context = \'CSS\'; ?>' . $match[2] . 'config->context = \'HTML\'; ?>"'; }, $content); // Inline scripts. $content = preg_replace_callback('#(?<=\s)(href="javascript:|pattern="|on[a-z]+=")([^"]*?)"#i', function($match) { return $match[1] . 'config->context = \'JS\'; ?>' . $match[2] . 'config->context = \'HTML\'; ?>"'; }, $content); // )#s']; $content = preg_replace_callback($regexp, function($match) use ($basepath) { $regexp = '#\b(url\([\'"]?)([^\'"\(\)]+)([\'"]?\))#'; $match[2] = preg_replace_callback($regexp, function($match) use ($basepath) { if ($this->template->isRelativePath($match[2] = trim($match[2]))) { $match[2] = $this->template->convertPath($match[2], $basepath); } return $match[1] . $match[2] . $match[3]; }, $match[2]); return $match[1] . $match[2] . $match[3]; }, $content); return $content; } /** * Convert PHP sections. * * Unlike v1, all variables in all PHP code belong to the same scope. * * Supported syntaxes: * * * {@ ... } * @php ... @endphp * * @param string $content * @return string */ protected function _convertPHPSections(string $content): string { $callback = function($match) { if ($match[1] === ''; return $open . self::_convertVariableScope(self::_removeContextSwitches($match[2])) . $close; }; $content = preg_replace_callback('#(<\?php|<\?=?)(.+?)(\?>)#s', $callback, $content); $content = preg_replace_callback('#(\{@)(.+?)(\})#s', $callback, $content); $content = preg_replace_callback('#(? '{$1}', '#(? '@{{', '#(? '@@$1', '#\$#' => '$', ]; $content = preg_replace_callback('#(@verbatim)\b(.+?)(@endverbatim)\b#s', function($match) use($conversions) { return preg_replace(array_keys($conversions), array_values($conversions), $match[2]); }, $content); return $content; } /** * Convert fragments. * * Sections delimited by ... (XE-style) * or @fragment('name') ... @endfragment (Blade-style) are stored * separately when executed, and can be accessed through getFragment() * afterwards. They are, of course, also included in the primary output. * * @param string $content * @return string */ protected function _convertFragments(string $content): string { // Convert XE-style fragment code. Blade-style is handled elsewhere. $regexp = '#(.*?)#s'; $content = preg_replace_callback($regexp, function($match) { $name = trim($match[1]); $content = $match[2]; $tpl = ''; $tpl .= $content; $tpl .= '_fragments[\$__last_fragment_name] = ob_get_flush(); ?>'; return $tpl; }, $content); return $content; } /** * Convert class aliases. * * This makes it easier to reference classes in deeply nested namespaces * without cluttering the template source code. * It works much the same way as the native "use" statement, * except that it does not actually import the class anywhere. * * XE-style syntax: * * * Blade-style syntax: * @use('Rhymix\Modules\Foobar\Models\HelloWorld', 'HelloWorldModel') * * @param string $content * @return string */ protected function _convertClassAliases(string $content): string { // Find all alias directives. $regexp = '#(?:|(?_aliases[$alias] = $class; return ''; }, $content); // Replace aliases. if (count($this->_aliases)) { $regexp = implode('|', array_map(function($str) { return preg_quote($str, '#'); }, array_keys($this->_aliases))); $content = preg_replace_callback('#\b(new\s+)?(' . $regexp . ')\b#', function($match) { return $match[1] . $this->_aliases[$match[2]]; }, $content); } return $content; } /** * Convert include directives. * * Templates can be included conditionally * using the 'if', 'when', and 'unless' attributes. * * Templates can be included with an array of variables that will * only be available in the included template. * If variables are supplied, global variables will not be available * inside of the included template. * * XE-style syntax: * * * * Blade-style syntax: * @include('view') * @include('view.blade.php', $vars) * * @param string $content * @return string */ protected function _convertIncludes(string $content): string { // Convert XE-style include directives. $regexp = '#()#'; $content = preg_replace_callback($regexp, function($match) { // Convert the path if necessary. $attrs = self::_getTagAttributes($match[1]); $path = $attrs['src'] ?? ($attrs['target'] ?? null); if (!$path) return $match[0]; $dir = '$this->relative_dirname'; if (preg_match('#^\^/?(\w.+)$#s', $path, $m)) { $dir = '"' . (str_contains($m[1], '/') ? dirname($m[1]) : '') . '"'; $path = basename($m[1]); } if (preg_match('#^(.+)/([^/]+)$#', $path, $match)) { $dir = '$this->normalizePath(' . $dir . ' . "' . $match[1] . '")'; $path = $match[2]; } // Generate the code to create a new Template object and compile it. $tpl = 'template->extension ?: 'auto') . '"); '; $tpl .= '$__tpl->setParent($this); if ($this->vars): $__tpl->setVars($this->vars); endif; '; $tpl .= !empty($attrs['vars']) ? '$__tpl->addVars(' . self::_convertVariableScope($attrs['vars']) . '); ' : ''; $tpl .= 'echo $__tpl->compile(); ?>'; // Add conditions around the code. if (!empty($attrs['if']) || !empty($attrs['when']) || !empty($attrs['cond'])) { $condition = $attrs['if'] ?? ($attrs['when'] ?? $attrs['cond']); $tpl = '' . $tpl . ''; } if (!empty($attrs['unless'])) { $condition = $attrs['unless']; $tpl = '' . $tpl . ''; } return self::_escapeVars($tpl); }, $content); // Convert Blade-style include directives. $parentheses = self::_getRegexpForParentheses(2); $regexp = '#(?_v2_include('%s', %s); ?>", $directive, $args); }, $content); // Handle the @each directive. $parentheses = self::_getRegexpForParentheses(1); $regexp = '#(?_v2_include("include", $__filename, [(string)$__varname => $__var]); '; $tpl .= 'endforeach; })(' . $args . '); ?>'; return self::_escapeVars($tpl); }, $content); return $content; } /** * Convert resource loading directives. * * This can be used to load nearly every kind of asset, from scripts * and stylesheets to lang files to Rhymix core Javascript plugins. * * XE-style syntax: * * * * * Blade-style syntax: * @load('dir/script.js', 'body', 10) * @load('^/common/js/plugins/ckeditor') * @load('styles.scss', $vars) * * @param string $content * @return string */ protected function _convertResource(string $content): string { // Convert XE-style load directives. $regexp = '#(<(load|unload)(?:\s+(?:target|src|type|media|index|vars)="(?:[^"]+)")+\s*/?>)#'; $content = preg_replace_callback($regexp, function($match) { $attrs = self::_getTagAttributes($match[1]); if ($match[2] === 'load') { return vsprintf('_v2_loadResource(%s, %s, %s, %s); ?>', [ var_export($attrs['src'] ?? ($attrs['target'] ?? ''), true), var_export($attrs['type'] ?? ($attrs['media'] ?? ''), true), var_export($attrs['index'] ?? '', true), self::_convertVariableScope($attrs['vars'] ?? '') ?: '[]', ]); } else { return vsprintf('', [ var_export($this->template->convertPath($attrs['src'] ?? ($attrs['target'] ?? '')), true), var_export($attrs['media'] ?? 'all', true), ]); } }, $content); // Convert Blade-style load directives. $parentheses = self::_getRegexpForParentheses(2); $regexp = '#(?_v2_loadResource(%s); ?>', $args); } else { return sprintf('convertPath(%s)); ?>', $args); } }, $content); return $content; } /** * Convert loop and condition directives. * * Loops and conditions can be written inside HTML comments (XE-style) * or without comments (Blade-style). * * It is highly recommended that ending directives match the starting * directive, e.g. @if ... @endif. However, for compatibility with legacy * templates, Rhymix will automatically find out which loop you are * trying to close if you simply write @end. Either way, your loops must * balance out or you will see 'unexpected end of file' errors. * * XE-style syntax: * * * * Blade-style syntax: * @if ($cond) * @endif * * @param string $content * @return string */ protected function _convertLoopDirectives(string $content): string { // Generate the list of directives to match. if (self::$_directives_regexp === null) { foreach (self::$_directives as $directive => $def) { $directive = preg_replace(['#(?<=\w)once$#', '#(?<=\w)if$#'], ['[Oo]nce', '[Ii]f'], $directive); $directives[] = $directive; if (count($def) > 1) { $directives[] = 'end[' . substr($directive, 0, 1) . strtoupper(substr($directive, 0, 1)) . ']' . substr($directive, 1); } } usort($directives, function($a, $b) { return strlen($b) - strlen($a); }); self::$_directives_regexp = implode('|', $directives) . '|end'; } // Convert both XE-style and Blade-style directives. $parentheses = self::_getRegexpForParentheses(2); $regexp = '#(?:)?#'; $content = preg_replace_callback($regexp, function($match) { // Collect the necessary information. $directive = strtolower($match[1]); $args = isset($match[2]) ? self::_convertVariableScope(substr($match[2], 1, -1)) : ''; $stack = null; $code = null; // If this is an ending directive, find the loop information in the stack. if (preg_match('#^end(.*)$#', $directive, $m)) { $stack = array_pop($this->_stack); $directive = $m[1] ?: ($stack ? $stack['directive'] : ''); } // Handle intermediate directives first. if ($directive === 'empty' && !$args && !$stack && end($this->_stack)['directive'] === 'forelse') { $stack = end($this->_stack); $code = self::$_directives['forelse'][1]; $code = strtr($code, ['%uniq' => $stack['uniq'], '%array' => $stack['array'], '%remainder' => $stack['remainder']]); } // Single directives. elseif (isset(self::$_directives[$directive]) && count(self::$_directives[$directive]) === 1) { $code = self::$_directives[$directive][0]; $code = str_contains($code, '%s') ? sprintf($code, $args) : $code; } // Paired directives. elseif (isset(self::$_directives[$directive])) { // Starting directive. if (!$stack) { $uniq = substr(sha1($this->template->absolute_path . ':' . $this->_uniq_order++), 0, 14); $array = ''; $remainder = ''; if ($directive === 'foreach' || $directive === 'forelse') { if (preg_match('#^(.+?)\sas\s(.+)#is', $args, $m)) { $array = trim($m[1]); $remainder = trim($m[2]); } else { $array = $args; $remainder = ''; } } $code = self::$_directives[$directive][0]; $code = strtr($code, ['%s' => $args, '%uniq' => $uniq, '%array' => $array, '%remainder' => $remainder]); $this->_stack[] = [ 'directive' => $directive, 'args' => $args, 'uniq' => $uniq, 'array' => $array, 'remainder' => $remainder, ]; } // Ending directive. else { $code = end(self::$_directives[$directive]); $code = strtr($code, ['%s' => $stack['args'], '%uniq' => $stack['uniq'], '%array' => $stack['array'], '%remainder' => $stack['remainder']]); } } else { return $match[0]; } // Put together the PHP code. return self::_escapeVars(""); }, $content); return $content; } /** * Convert inline directives. * * This helps display commonly used attributes conditionally, * without littering the template with inline @if ... @endif directives. * The 'cond' attribute usd in XE has been expanded to support * the shorter 'if' notation, 'when', and 'unless'. * * XE-style syntax: *