['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;',
],
'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;',
],
'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;'],
'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;
// 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->_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);
// 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|poster)="([^"]+)"#';
return preg_replace_callback($regexp, function($match) use ($basepath) {
if ($match[2] !== 'srcset')
{
$src = trim($match[3]);
return $match[1] . sprintf('%s="%s"', $match[2], $this->template->isRelativePath($src) ? $this->template->convertPath($src, $basepath) : $src);
}
else
{
$srcset = array_map('trim', explode(',', $match[3]));
$result = array_map(function($src) use($basepath) {
return $this->template->isRelativePath($src) ? $this->template->convertPath($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('#(? ... (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 = '#(?: