['if (%s):', 'endif;'], 'unless' => ['if (!(%s)):', 'endif;'], 'for' => ['for (%s):', 'endfor;'], 'while' => ['while (%s):', 'endwhile;'], 'switch' => ['switch (%s):', 'endswitch;'], 'foreach' => [ '$__tmp_%uniq = %array ?? []; foreach ($__tmp_%uniq as %remainder):', 'endforeach;', ], 'forelse' => [ '$__tmp_%uniq = %array ?? []; if($__tmp_%uniq): foreach ($__tmp_%uniq as %remainder):', 'endforeach; else:', 'endif;', ], 'once' => [ "if (!isset(\$GLOBALS['tplv2_once']['%uniq'])):", "endif; \$GLOBALS['tplv2_once']['%uniq'] = true;", ], 'isset' => ['if (isset(%s)):', 'endif;'], 'unset' => ['if (!isset(%s)):', 'endif;'], 'empty' => ['if (empty(%s)):', 'endif;'], 'admin' => ['if ($this->user->isAdmin()):', 'endif;'], 'auth' => ['if ($this->user->isMember()):', 'endif;'], 'member' => ['if ($this->user->isMember()):', 'endif;'], 'guest' => ['if (!$this->user->isMember()):', 'endif;'], 'desktop' => ['if (!$__Context->m):', 'endif;'], 'mobile' => ['if ($__Context->m):', 'endif;'], 'else' => ['else:'], 'elseif' => ['elseif (%s):'], 'case' => ['case %s:'], 'default' => ['default:'], 'continue' => ['continue;'], 'break' => ['break;'], ]; /** * 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; $this->source_type = preg_match('!^((?:m\.)?[a-z]+)/!', $template->relative_dirname, $match) ? $match[1] : null; // Preprocessing. $content = $this->_preprocess($content); // Apply conversions. $content = $this->_addContextSwitches($content); $content = $this->_removeComments($content); $content = $this->_convertRelativePaths($content); $content = $this->_convertPHPSections($content); $content = $this->_convertVerbatimSections($content); $content = $this->_convertClassAliases($content); $content = $this->_convertIncludes($content); $content = $this->_convertAssets($content); $content = $this->_convertLoopDirectives($content); $content = $this->_convertInlineDirectives($content); $content = $this->_convertMiscDirectives($content); $content = $this->_convertEchoStatements($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 { return preg_replace_callback('#(]*)?|config->context = "HTML"; ?>' . $match[1]; } else { return $match[1] . 'config->context = "JS"; ?>'; } }, $content); } /** * Remove comments that should not be visible in the output. * * * {{-- Blade-style Comment --}} * * @param string $content * @return string */ protected function _removeComments(string $content): string { return preg_replace([ '##', '#\{\{--[^\n]+?--\}\}#', ], '', $content); } /** * Convert relative paths to absolute paths. * * @param string $content * @return string */ protected function _convertRelativePaths(string $content): string { // Get the base path for this template. $basepath = \RX_BASEURL . $this->template->relative_dirname; // Convert all src and srcset attributes. $regexp = '#(<(?:img|audio|video|script|input|source|link)\s[^>]*)(src|srcset)="([^"]+)"#'; return preg_replace_callback($regexp, function($match) use ($basepath) { if ($match[2] === 'src') { $src = trim($match[3]); return $match[1] . sprintf('src="%s"', self::_isRelativePath($src) ? self::_convertRelativePath($src, $basepath) : $src); } else { $srcset = array_map('trim', explode(',', $match[3])); $result = array_map(function($src) use($basepath) { return self::_isRelativePath($src) ? self::_convertRelativePath($src, $basepath) : $src; }, array_filter($srcset, function($src) { return !empty($src); })); return $match[1] . sprintf('srcset="%s"', implode(', ', $result)); } }, $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) { $open = ''; return $open . self::_convertVariableScope($match[2]) . $close; }; $content = preg_replace_callback('#(<\?php|<\?(?!=))(.+?)(\?>)#s', $callback, $content); $content = preg_replace_callback('#(\{@)(.+?)(\})#s', $callback, $content); $content = preg_replace_callback('#(? * * 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 = '#^[\x09\x20]*(?:|(?_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 = '#^[\x09\x20]*()[\x09\x20]*$#m'; $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]); } // Generate the code to create a new Template object and compile it. $tpl = 'template->extension ?: 'auto') . '"); '; $tpl .= !empty($attrs['vars']) ? '$__tpl->setVars(' . 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. $regexp = '#^[\x09\x20]*(?template->extension === 'blade.php' ? 'blade.php' : 'html'; $dir = '$this->relative_dirname'; if ($match[1] === 'include' || $match[1] === 'includeIf') { $path = preg_match('#^([\'"])([^\'"]+)\1#', $match[2], $m) ? $m[2] : ''; if (preg_match('#^\^/?(\w.+)$#s', $path, $mm)) { $dir = '"' . escape_dqstr(str_contains($mm[1], '/') ? dirname($mm[1]) : '') . '"'; $filename = basename($mm[1]); $match[2] = preg_replace('#^([\'"])([^\'"]+)\1#', '$1' . $filename . '$1', $match[2]); } } else { $path = preg_match('#^([^,]+,\s*)([\'"])([^\'"]+)\2#', $match[2], $m) ? $m[3] : ''; if (preg_match('#^\^/?(\w.+)$#s', $path, $mm)) { $dir = '"' . escape_dqstr(str_contains($mm[1], '/') ? dirname($mm[1]) : '') . '"'; $filename = basename($mm[1]); $match[2] = preg_replace('#^([^,]+,\s*)([\'"])([^\'"]+)\2#', '$1$2' . $filename . '$2', $match[2]); } } // Generate an IIFE to create a new Template object and compile it. if ($match[1] === 'include') { $tpl = 'setVars($__vars); ' ; $tpl .= 'echo $__tpl->compile(); })(' . $dir . ', ' . $match[2] . '); ?>'; } elseif ($match[1] === 'includeIf') { $tpl = 'exists()) return; '; $tpl .= 'if ($__vars) $__tpl->setVars($__vars); ' ; $tpl .= 'echo $__tpl->compile(); })(' . $dir . ', ' . $match[2] . '); ?>'; } else { $tpl = 'setVars($__vars); ' ; $tpl .= 'echo $__tpl->compile(); })("' . $match[1] . '", ' . $dir . ', ' . $match[2] . '); ?>'; } return self::_escapeVars($tpl); }, $content); return $content; } /** * Convert asset 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 _convertAssets(string $content): string { // Convert XE-style load directives. $regexp = '#^[\x09\x20]*()[\x09\x20]*$#m'; $content = preg_replace_callback($regexp, function($match) { $attrs = self::_getTagAttributes($match[1]); $attrs['src'] = $attrs['src'] ?? ($attrs['target'] ?? null); if (!$attrs['src']) return $match[0]; return self::_escapeVars(self::_generateCodeForAsset($attrs)); }, $content); // Convert Blade-style load directives. $regexp = '#^[\x09\x20]*(?template->relative_dirname); } else { $external = true; } // Determine the type of resource. if (!$external && str_starts_with($path, './common/js/plugins/')) { $restype = 'jsplugin'; } elseif (!$external && preg_match('#/lang(\.xml)?$#', $path)) { $restype = 'lang'; } elseif (preg_match('#\.(css|js|scss|less)($|\?|/)#', $path, $match)) { $restype = $match[1]; } else { $restype = 'unknown'; } // Generate code for each type of asset. if ($restype === 'jsplugin') { if (preg_match('#/common/js/plugins/([^/]+)#', $path, $match)) { $plugin_name = $match[1]; return sprintf('', var_export($plugin_name, true)); } else { return sprintf('', var_export($path, true)); } } elseif ($restype === 'lang') { $lang_dir = preg_replace('#/lang\.xml$#', '', $path); return sprintf('', var_export($lang_dir, true)); } elseif ($restype === 'js') { return vsprintf('', [ var_export($path, true), var_export($info['type'] ?? '', true), var_export($external ? $this->source_type : '', true), var_export(isset($info['index']) ? intval($info['index']) : '', true), ]); } elseif ($restype === 'unknown') { return sprintf('', var_export($path, true)); } else { return vsprintf('', [ var_export($path, true), var_export($info['media'] ?? '', true), var_export($external ? $this->source_type : '', true), var_export(isset($info['index']) ? intval($info['index']) : '', true), empty($info['vars']) ? '[]' : self::_convertVariableScope($info['vars']) ]); } } /** * Convert loop and condition directives. * * Loops and conditions can be written inside HTML comments (XE-style) * or without comments (Blade-style). If using the Blade-style syntax, * each directive must appear in its own line. * * 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 end 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 { // Convert block directives. $regexp = '#(?:^[\x09\x20]*||[\x09\x20]*$)#sm'; $content = preg_replace_callback($regexp, function($match) { // Collect the necessary information. $directive = $match[1]; $args = isset($match[2]) ? self::_convertVariableScope($match[2]) : ''; $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['directive']; } // Handle intermediate directives first. if ($directive === 'empty' && !$args && !$stack && end($this->_stack)['directive'] === 'forelse') { $code = self::$_loopdef['forelse'][1]; } // Single directives. elseif (isset(self::$_loopdef[$directive]) && count(self::$_loopdef[$directive]) === 1) { $code = self::$_loopdef[$directive][0]; $code = str_contains($code, '%s') ? sprintf($code, $args) : $code; } // Paired directives. elseif (isset(self::$_loopdef[$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::$_loopdef[$directive][0]; $code = strtr($code, ['%uniq' => $uniq, '%array' => $array, '%remainder' => $remainder]); $code = str_contains($code, '%s') ? sprintf($code, $args) : $code; $this->_stack[] = [ 'directive' => $directive, 'uniq' => $uniq, 'array' => $array, 'remainder' => $remainder, ]; } // Ending directive. else { $code = end(self::$_loopdef[$directive]); $code = strtr($code, ['%uniq' => $stack['uniq'], '%array' => $stack['array'], '%remainder' => $stack['remainder']]); $code = str_contains($code, '%s') ? sprintf($code, $args) : $code; } } 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: *