diff --git a/classes/template/TemplateHandler.class.php b/classes/template/TemplateHandler.class.php
index 034875e42..255ee702b 100644
--- a/classes/template/TemplateHandler.class.php
+++ b/classes/template/TemplateHandler.class.php
@@ -1,11 +1,13 @@
config = new \stdClass;
+ $this->config->version = 1;
+ $this->config->autoescape = false;
+
+ // Set user information.
+ $this->user = Session::getMemberInfo() ?: new Helpers\SessionHelper();
+
+ // Cache commonly used configurations as static properties.
+ if (self::$_mtime === null)
+ {
+ self::$_mtime = filemtime(__FILE__);
+ }
+ if (self::$_delay_compile === null)
+ {
+ self::$_delay_compile = config('view.delay_compile') ?? 0;
+ }
+
+ // If paths were provided, initialize immediately.
+ if ($dirname && $filename)
+ {
+ $this->_setSourcePath($dirname, $filename);
+ }
+ }
+
+ /**
+ * Initialize and normalize paths.
+ *
+ * @param string $dirname
+ * @param string $filename
+ * @return void
+ */
+ protected function _setSourcePath(string $dirname, string $filename): void
+ {
+ // Normalize the template path. Result will look like 'modules/foo/views/'
+ $dirname = trim(preg_replace('@^' . preg_quote(\RX_BASEDIR, '@') . '|\./@', '', strtr($dirname, ['\\' => '/', '//' => '/'])), '/') . '/';
+ $dirname = preg_replace('/[\{\}\(\)\[\]<>\$\'"]/', '', $dirname);
+ $this->absolute_dirname = \RX_BASEDIR . $dirname;
+ $this->relative_dirname = $dirname;
+
+ // Normalize the filename. Result will look like 'bar/example.html'
+ $filename = trim(strtr($filename, ['\\' => '/', '//' => '/']), '/');
+ $filename = preg_replace('/[\{\}\(\)\[\]<>\$\'"]/', '', $filename);
+ if (!preg_match('/\.(html?|php)$/', $filename))
+ {
+ $filename .= '.html';
+ }
+ $this->filename = $filename;
+ $this->absolute_path = $this->absolute_dirname . $filename;
+ $this->relative_path = $this->relative_dirname . $filename;
+ $this->exists = Storage::exists($this->absolute_path);
+ $this->_setCachePath();
+ }
+
+ /**
+ * Set the path for the cache file.
+ *
+ * @return void
+ */
+ protected function _setCachePath()
+ {
+ $this->cache_path = \RX_BASEDIR . 'files/cache/template/' . $this->relative_path . '.php';
+ if ($this->exists)
+ {
+ Debug::addFilenameAlias($this->absolute_path, $this->cache_path);
+ }
+ }
+
+ /**
+ * Compile and execute a template file.
+ *
+ * You don't need to pass any paths if you have already supplied them
+ * through the constructor. They exist for backward compatibility.
+ *
+ * $override_filename should be considered deprecated, as it is only
+ * used in faceOff (layout source editor).
+ *
+ * @param ?string $dirname
+ * @param ?string $filename
+ * @param ?string $override_filename
+ * @return string
+ */
+ public function compile(?string $dirname = null, ?string $filename = null, ?string $override_filename = null)
+ {
+ // If paths are given, initialize now.
+ if ($dirname && $filename)
+ {
+ $this->_setSourcePath($dirname, $filename);
+ }
+ if ($override_filename)
+ {
+ $override_filename = trim(preg_replace('@^' . preg_quote(\RX_BASEDIR, '@') . '|\./@', '', strtr($override_filename, ['\\' => '/', '//' => '/'])), '/') . '/';
+ $override_filename = preg_replace('/[\{\}\(\)\[\]<>\$\'"]/', '', $override_filename);
+ $this->absolute_path = \RX_BASEDIR . $override_filename;
+ $this->relative_path = $override_filename;
+ $this->exists = Storage::exists($this->absolute_path);
+ $this->_setCachePath();
+ }
+
+ // Return error if the source file does not exist.
+ if (!$this->exists)
+ {
+ $error_message = sprintf('Template not found: %s', $this->relative_path);
+ trigger_error($error_message, \E_USER_WARNING);
+ return escape($error_message);
+ }
+
+ // Record the starting time.
+ $start = microtime(true);
+
+ // Find the latest mtime of the source template and the template parser.
+ $filemtime = filemtime($this->absolute_path);
+ if ($filemtime > time() - self::$_delay_compile)
+ {
+ $latest_mtime = self::$_mtime;
+ }
+ else
+ {
+ $latest_mtime = max($filemtime, self::$_mtime);
+ }
+
+ // If a cached result does not exist, or if it is stale, compile again.
+ if (!Storage::exists($this->cache_path) || filemtime($this->cache_path) < $latest_mtime)
+ {
+ $content = $this->_convert();
+ if (!Storage::write($this->cache_path, $content))
+ {
+ throw new Exception('Cannot write template cache file: ' . $this->cache_path);
+ }
+ }
+
+ $output = $this->_execute();
+
+ // Record the time elapsed.
+ $elapsed_time = microtime(true) - $start;
+ if (!isset($GLOBALS['__template_elapsed__']))
+ {
+ $GLOBALS['__template_elapsed__'] = 0;
+ }
+ $GLOBALS['__template_elapsed__'] += $elapsed_time;
+
+ return $output;
+ }
+
+ /**
+ * Compile a template and return the PHP code.
+ *
+ * @param string $dirname
+ * @param string $filename
+ * @return string
+ */
+ public function compileDirect(string $dirname, string $filename): string
+ {
+ // Initialize paths. Return error if file does not exist.
+ $this->_setSourcePath($dirname, $filename);
+ if (!$this->exists)
+ {
+ $error_message = sprintf('Template not found: %s', $this->relative_path);
+ trigger_error($error_message, \E_USER_WARNING);
+ return escape($error_message);
+ }
+
+ // Convert, but don't actually execute it.
+ return $this->_convert();
+ }
+
+ /**
+ * Convert template code to PHP using a version-specific parser.
+ *
+ * Directly passing $content as a string is not available as an
+ * official API. It only exists for unit testing.
+ *
+ * @return string
+ */
+ protected function _convert(?string $content = null): string
+ {
+ // Read the source, or use the provided content.
+ if ($content === null && $this->exists)
+ {
+ $content = Storage::read($this->absolute_path);
+ $content = trim($content) . PHP_EOL;
+ }
+ if ($content === null || $content === '')
+ {
+ return '';
+ }
+
+ // Remove UTF-8 BOM and convert CRLF to LF.
+ $content = preg_replace(['/^\xEF\xBB\xBF/', '/\r\n/'], ['', "\n"], $content);
+
+ // Check the config tag: or
+ $content = preg_replace_callback('!^!', function($match) {
+ $this->config->{$match[1]} = ($match[1] === 'version' ? intval($match[2]) : toBool($match[2]));
+ return sprintf('config->%s = %s; ?>', $match[1], var_export($this->config->{$match[1]}, true));
+ }, $content);
+
+ // Turn autoescape on if the version is 2 or greater.
+ if ($this->config->version >= 2)
+ {
+ $this->config->autoescape = true;
+ }
+
+ // Call a version-specific parser to convert template code into PHP.
+ $class_name = '\Rhymix\Framework\Parsers\Template\TemplateParser_v' . $this->config->version;
+ $parser = new $class_name;
+ $content = $parser->convert($content, $this);
+
+ return $content;
+ }
+
+ /**
+ * Execute the converted template and return the output.
+ *
+ * @return string
+ */
+ protected function _execute(): string
+ {
+ // Import Context and lang as local variables.
+ $__Context = \Context::getAll();
+ global $lang;
+
+ // Start the output buffer.
+ $this->ob_level = ob_get_level();
+ ob_start();
+
+ // Include the compiled template.
+ include $this->cache_path;
+
+ // Fetch the content of the output buffer until the buffer level is the same as before.
+ $content = '';
+ while (ob_get_level() > $this->ob_level)
+ {
+ $content .= ob_get_clean();
+ }
+
+ // Insert comments for debugging.
+ if(Debug::isEnabledForCurrentUser() && \Context::getResponseMethod() === 'HTML' && !preg_match('/^<(?:\!DOCTYPE|\?xml)/', $content))
+ {
+ $meta = '' . "\n";
+ $content = sprintf($meta, 'Start') . $content . sprintf($meta, 'End');
+ }
+
+ return $content;
+ }
+}
diff --git a/common/framework/parsers/template/TemplateParser_v1.php b/common/framework/parsers/template/TemplateParser_v1.php
new file mode 100644
index 000000000..409d977d3
--- /dev/null
+++ b/common/framework/parsers/template/TemplateParser_v1.php
@@ -0,0 +1,899 @@
+autoescape_config_exists = str_contains($content, '$this->config->autoescape = ');
+ $this->source_type = preg_match('!^((?:m\.)?[a-z]+)/!', $template->relative_dirname, $matches) ? $matches[1] : null;
+ $this->template = $template;
+
+ // replace comments
+ $content = preg_replace('@@s', '', $content);
+
+ // replace value of src in img/input/script tag
+ $content = preg_replace_callback('/<(?:img|input|script)(?:[^<>]*?)(?(?=cond=")(?:cond="[^"]+"[^<>]*)+|)[^<>]* src="(?!(?:https?|file|data):|[\/\{])([^"]+)"/is', array($this, '_replacePath'), $content);
+
+ // replace value of srcset in img/source/link tag
+ $content = preg_replace_callback('/<(?:img|source|link)(?:[^<>]*?)(?(?=cond=")(?:cond="[^"]+"[^<>]*)+|)[^<>]* srcset="([^"]+)"/is', array($this, '_replaceSrcsetPath'), $content);
+
+ // replace loop and cond template syntax
+ $content = $this->_parseInline($content);
+
+ // include, unload/load, import
+ $content = preg_replace_callback('/{(@[\s\S]+?|(?=[\$\\\\]\w+|_{1,2}[A-Z]+|[!\(+-]|\w+(?:\(|::)|\d+|[\'"].*?[\'"]).+?)}|<(!--[#%])?(include|import|(un)?load(?(4)|(?:_js_plugin)?)|config)(?(2)\(["\']([^"\']+)["\'])(.*?)(?(2)\)--|\/)>|(\s*)/', array($this, '_parseResource'), $content);
+
+ // remove block which is a virtual tag
+ $content = preg_replace('@?block\s*>@is', '', $content);
+
+ // form auto generation
+ $temp = preg_replace_callback('/(