engine */ 'php' => 'php', ]; /** @var array Views shared data */ protected $sharedData = []; /** @var Layout */ protected $layout; /** @var string текущий layout */ protected $layoutTemplate = 'main'; /** @var array данные текущей страницы */ protected $pageData = []; /** @var array данные текущей страницы для отправки в JS */ protected $jsData = []; /** @var array registered packages */ protected $packagesList = []; /** @var array packages to include */ protected $packages = []; /** @var array scripts to include */ protected $scripts = []; /** @var array inline scripts */ protected $scriptsInline = []; /** @var array open inline scripts (currently building) */ protected $scriptsInlineOpen = []; /** @var int inline scripts default position */ protected $scriptsInlineDefaultPosition = self::POS_FOOT; /** @var array styles to include */ protected $styles = []; /** * Manifest filename * @var string */ public const MANIFEST_FILE = 'mix-manifest.json'; /** @var array manifest files content cache */ protected $manifestFiles = []; public function __construct(\bff\contracts\Application $app, Filesystem $files, ?string $path = null) { $this->app = $app; $this->files = $files; if (!empty($path)) { $this->addPath($path); } else { $this->addPath($this->app->templatesPath()); $this->addPath($this->app->templatesPath('', !$this->app->adminPanel())); } # Register core packages $this->packagesList = $this->app->filter('view.packages.list', [ # admin 'fancybox' => [ # v1.3.4 'js' => '@core/admin/fancybox/fancybox.js', 'css' => '@core/admin/fancybox/style.css', ], 'fancybox2' => [ # v2.1.5 'js' => '@core/fancybox2/jquery.fancybox.pack.js', 'css' => '@core/fancybox2/jquery.fancybox.css', ], 'fancybox3' => [ # v3.5.7 'js' => '@core/fancybox3/jquery.fancybox.js', 'css' => '@core/fancybox3/jquery.fancybox.css', ], 'datepicker' => [ 'js' => '@core/admin/datepicker/datepicker.js', 'css' => '@core/admin/datepicker/style.css', ], 'datepicker.bs' => [ # ? 'js' => '@core/admin/bootstrap-datepicker/js/bootstrap-datepicker.min.js', 'css' => '@core/admin/bootstrap-datepicker/css/bootstrap-datepicker3.min.css', ], 'tablednd' => [ 'js' => '@core/admin/tablednd.js', ], 'comments' => [ 'js' => '@core/admin/comments/comments.js', 'css' => '@core/admin/comments/style.css', ], # common 'autocomplete' => [ 'js' => '@core/autocomplete/autocomplete.js', ], 'autocomplete.fb' => [ 'js' => '@core/autocomplete.fb/autocomplete.fb.js', 'css' => '@core/autocomplete.fb/style.css', ], 'dynprops' => [ 'js' => '@core/dynprops/dynprops.min.js', ], 'history' => [ # deprecated 'js' => '@core/history/history.min.js', ], 'jcrop' => [ # deprecated 'js' => '@core/jcrop/jquery.jcrop.min.js', 'css' => '@core/jcrop/jquery.Jcrop.css', ], 'jquery' => [ # deprecated 'js' => '@core/jquery/jquery.min.js', ], 'perfect-scrollbar' => [ 'js' => '@core/perfect-scrollbar/perfect-scrollbar.min.js', 'css' => '@core/perfect-scrollbar/perfect-scrollbar.css', ], 'publicator' => [ 'js' => '@core/publicator/publicator.min.js', 'css' => '@core/publicator/style.css', ], 'publicator.frontend' => [ 'js' => '@core/publicator/frontend.min.js', 'css' => '@core/publicator/frontend.css', ], 'qquploader' => [ 'js' => '@core/qquploader/fileuploader.js', 'css' => '@core/qquploader/style.css', ], 'tinymce' => [ 'js' => [ '@core/tinymce/tinymce.min.js', '@core/tinymce/jquery.tinymce.min.js', ], ], 'ui.sortable' => [ 'js' => [ '@core/jquery.ui/core.js', # v1.8.22 '@core/jquery.ui/sortable.js', # v1.8.22 ], ], 'ui.sortable.last' => [ 'js' => '@core/jquery.ui/sortable.last.js', # v1.12.1 ], 'wysiwyg' => [ 'js' => '@core/wysiwyg/wysiwyg.min.js', 'css' => '@core/wysiwyg/style.css', ], ]); } public function onNewRequest($request) { $this->reset(); } /** * Get the filesystem instance * @return Filesystem */ public function getFiles() { return $this->files; } /** * Add a views path * @param string $path * @param bool $prepend * @return void */ public function addPath(string $path, bool $prepend = false) { $path = $this->resolvePath($path); if ($prepend) { array_unshift($this->paths, $path); } else { $this->paths[] = $path; } } /** * Resolve the path * @param string $path * @return string */ protected function resolvePath(string $path) { return $path; } /** * Register view extension * @param string $extension * @param string $engine * @return void */ public function addExtension(string $extension, string $engine) { if (array_key_exists($extension, $this->extensions)) { unset($this->extensions[$extension]); } $this->extensions = [$extension => $engine] + $this->extensions; } /** * View registered package * @param string $name * @param array|Closure|null $resources to register/update package * @return void */ public function package(string $name, $resources = null) { if ($resources) { $this->packageRegister($name, $resources); } # include once if (! array_key_exists($name, $this->packages)) { $this->packages[$name] = true; $package = $this->packagesList[$name] ?? null; if ($package) { $this->packageInclude($package); } } } /** * Register package * @param string $name * @param array|Closure $resources * @return void */ public function packageRegister(string $name, $resources) { $this->packagesList[$name] = $resources; } /** * Register packages * @param array $packages * @return void */ public function packagesRegister(array $packages) { foreach ($packages as $name => $package) { $this->packageRegister($name, $package); } } /** * Include package * @param array|Closure $package * @param string|null $resource type * @return void */ protected function packageInclude($package, ?string $resource = null) { if ($package instanceof Closure) { $package = $package(); } if (! is_array($package)) { return; } $resources = (!is_null($resource) ? [$resource] : ['js', 'css']); foreach ($resources as $resource) { if (empty($package[$resource])) { continue; } if ($resource === 'js') { $this->script($package[$resource]); } elseif ($resource === 'css') { $this->style($package[$resource]); } } } /** * Получаем данные текущей страницы * @param string $key ключ требуемых данных * @param bool $default значение по умолчанию * @return mixed */ public function pageData(string $key, $default = false) { return $this->pageData[$key] ?? $default; } /** * Сохраняем данные текущей страницы * @param string|array $key ключ данных * @param mixed $value значение * @return void */ public function setPageData($key, $value = false) { if (is_string($key)) { $this->pageData[$key] = $value; } elseif (is_array($key)) { foreach ($key as $k => $v) { if (is_string($k)) { $this->pageData[$k] = $v; } } } } /** * Set data to transfer to javascript * @param string|array $key unique data key or [key => data, ...] * @param mixed $data * @param bool $merge * @return void */ public function jsData($key, $data = null, $merge = true) { if (is_string($key)) { if ( $merge && is_array($data) && array_key_exists($key, $this->jsData) && is_array($this->jsData[$key]) ) { $this->jsData[$key] = array_merge($this->jsData[$key], $data); } else { $this->jsData[$key] = $data; } } elseif (is_array($key)) { foreach ($key as $k => $v) { $this->jsData($k, $v, $merge); } } } /** * Render js data to transfer to javascript * @param array $opts [ * string|bool 'escape' true/'html'/'js' * int 'jsonOptions' json options * ] * @return mixed */ public function renderJsData(array $opts = []) { $opts = $this->defaults($opts, [ 'key' => null, 'escape' => false, 'jsonOptions' => JSON_UNESCAPED_UNICODE, ]); if (empty($this->jsData)) { $opts['jsonOptions'] += JSON_FORCE_OBJECT; } return HTML::escape(json_encode( $this->jsData[$opts['key'] ?? null] ?? $this->jsData, $opts['jsonOptions'] ), $opts['escape']); } /** * Начало буферизации вывода * @return void */ public function start() { ob_start(); ob_implicit_flush(false); } /** * Окончание буферизации вывода * @return string */ public function stop(): string { return ltrim(ob_get_clean()); } /** * Add shared views data * @param string|array $key * @param mixed $value * @return mixed */ public function share($key, $value = null) { if (is_array($key)) { foreach ($key as $k => $v) { $this->sharedData[$k] = $v; } } else { $this->sharedData[$key] = $value; } return $value; } /** * Get views shared data * @param string|null $key * @param mixed $default * @return array|mixed */ public function getShared($key = null, $default = null) { if (is_null($key)) { return $this->sharedData; } return $this->sharedData[$key] ?? $default; } /** * Render template * @param array $data @ref template data * @param string $view filename without extension * @param string|null $path absolute path to file (without filename) * @param array $opts [hookPrefix:string, this:object, tags:bool, throw:bool] * @return string|ResponseInterface|mixed */ public function render(array &$data, string $view, ?string $path, array $opts = []) { # view => file path $filePath = $this->resolveViewPath($view, $path, $opts); if (empty($filePath)) { if ($opts['throw'] ?? true) { throw new Exception(sprintf('Unable to resolve file for view "%s" in path "%s"', $view, $path)); } return ''; } # hook: before render (data) $hookPrefix = $opts['hookPrefix'] ?? 'view.tpl'; if (empty($path) || $path === $this->app->templatesPath()) { $hookPrefix = 'view.tpl'; } $hook = $hookPrefix . '.' . $view; $hookData = [ 'data' => &$data, 'filePath' => $filePath, 'fileName' => $this->files->name($filePath), ]; $this->app->hook($hook . '.data', $hookData); # render $render = function ($__filePath, $__sharedData, &$aData) { extract($__sharedData, EXTR_SKIP); extract($aData, EXTR_REFS | EXTR_OVERWRITE); return require $__filePath; }; if (array_key_exists('this', $opts)) { # bind context $render = $render->bindTo($opts['this'], $opts['this']); } $obLevel = ob_get_level(); try { $this->start(); $response = $render($filePath, $this->sharedData, $data); $content = $this->stop(); } catch (Throwable $e) { while (ob_get_level() > $obLevel) { ob_end_clean(); } throw $e; } if ($response instanceof ResponseInterface) { return $response; } # hook: after render if ($this->app->hooksAdded($hook)) { unset($hookData['data']); $content = $this->app->filter($hook, $content, $data, $hookData); } # tags: process if ($opts['tags'] ?? false) { $content = $this->app->tagsProcess($content); } return $content; } /** * Hook on before template render * @param string $template * @param callable $callback function ($data = [&data, filePath, fileName]) {} * @param string|null $from module.{name}, plugin.{name}, null => /tpl/ * @param int|null $priority * @return mixed */ public function beforeRender(string $template, callable $callback, ?string $from = null, ?int $priority = null) { return $this->app->hookAdd('view.' . ($from ?: 'tpl') . '.' . $template . '.data', $callback, $priority); } /** * Hook on after template render * @param string $template * @param callable $callback function ($content) { return $content; } * @param string|null $from module.{name}, plugin.{name}, null => /tpl/ * @param int|null $priority * @return mixed */ public function afterRender(string $template, callable $callback, ?string $from = null, ?int $priority = null) { return $this->app->hookAdd('view.' . ($from ?: 'tpl') . '.' . $template, $callback, $priority); } /** * Рендеринг шаблона * @param string $view название шаблона (без расширения) * @param array $data данные, которые необходимо передать в шаблон * @param string|null $from название/объект модуля/плагина, путь к файлу, null - путь к шаблона по умолчанию * @param array $opts * @return string|mixed HTML */ public function template(string $view, array $data = [], $from = null, array $opts = []) { if (empty($from) && strpos(trim($view), static::HINT_DELIMITER) > 0) { $segments = explode(static::HINT_DELIMITER, $view); if (count($segments) === 2) { [$from, $view] = $segments; } } if (is_string($from) && !empty($from)) { if ($this->app->moduleExists($from)) { return $this->app->module($from)->template($view, $data, $opts); } elseif ($this->app->pluginExists($from)) { return $this->app->plugin($from)->template($view, $data, $opts); } } elseif ($from instanceof Module) { return $from->template($view, $data, $opts); } return $this->render($data, $view, $from, $opts); } /** * Формирование пути к файлу шаблона * @param string $view имя файла шаблона без расширения * @param string|null $path путь к файлу * @param array $opts [custom, extension] * @return string|bool */ public function resolveViewPath(string $view, ?string $path = null, array $opts = []) { # paths $paths = $this->paths; if (! empty($path)) { array_unshift( $paths, $this->resolvePath($path) ); } # extensions + subdirs $dot = strpos($view, '.'); $views = []; foreach ($this->extensions as $extension => $engine) { $views[] = $view . '.' . $extension; if ($dot !== false) { # sub dir: view.file => view/file (first dot only) $views[] = substr_replace($view, DS, $dot, 1) . '.' . $extension; } } # file path $filePath = false; foreach ($paths as $path) { foreach ($views as $view) { $filePath = $this->resolveFileInPath($view, $path, $opts); if ($filePath !== false) { break 2; } } } return $filePath; } /** * Проверям наличие файла по указанному пути * Учитываем активную тему и кастомизацию * @param string $fileName * @param string $dir * @param array $opts [extension:bool, theme:?bool, custom:bool] * @return string|bool */ public function resolveFileInPath(string $fileName, string $dir, array $opts = []) { # relative $relPath = DS . rtrim(mb_substr($dir, mb_strlen($this->app->basePath())), DS . ' '); # extension + themed file version $extension = $opts['extension'] ?? (mb_stripos($relPath, DS . 'plugins') === 0); if ($extension) { $themedPath = $this->resolvePath(rtrim($dir, DS) . DS . '_' . $this->app->theme()->getName()); $filePath = $this->resolveFileInPath($fileName, $themedPath, ['extension' => false] + $opts); if ($filePath) { return $filePath; } } # active theme if (($opts['theme'] ?? true) !== false) { $fileInTheme = $this->resolveFileInTheme(rtrim($relPath, DS) . DS . $fileName); if ($fileInTheme !== false) { $filePath = $fileInTheme; } else { $filePath = rtrim($dir, DS . ' ') . DS . $fileName; } } else { $filePath = rtrim($dir, DS . ' ') . DS . $fileName; } # do not try to resolve relative /file if ($filePath === DS . $fileName) { return false; } # modification $filePath = modification($filePath, $opts['custom'] ?? true); if ($this->files->exists($filePath)) { return $filePath; } return false; } /** * Look for themed version of the file * @param string $filePath relative file path * @param bool $asUrl * @param array $opts [&version] * @return string|bool path/url to current theme (if file in theme was found) */ public function resolveFileInTheme(string $filePath, bool $asUrl = false, array $opts = []) { $themePath = $this->app->theme()->fileThemed($filePath, $asUrl, $opts); if ($themePath !== false) { return $themePath . $filePath; } return false; } /** * Render final html response * @param array $data * @param \bff\http\Response|ResponseInterface|null $response * @return \bff\http\Response|ResponseInterface */ public function layoutResponse(array $data, ?ResponseInterface $response = null) { $response = $response ?? $this->app->response(); # Render layout and add to response if (! empty($this->layoutTemplate)) { if ($response->getBody()->isWritable()) { $response->getBody()->write( $this->layoutRender($data, $this->layoutTemplate) ); } } return $response; } /** * Рендеринг layout шаблона * @param array $data данные, которые необходимо передать в шаблон * @param string|null $template название layout'a (без расширения) * @param string|null $path путь к шаблону или false - используем templatesPath * @param array|null $opts * @return mixed */ public function layoutRender( array $data, ?string $template = null, ?string $path = null, array $opts = null ) { if (empty($template)) { $template = $this->getLayoutTemplate(); if (empty($template)) { $template = 'main'; } } $layout = $this->getLayout(); $opts['this'] = $opts['this'] ?? $layout; $opts['tags'] = true; if ( $this->app->adminPanel() || $this->resolveViewPath($layout->getTemplate()) === false # todo ) { return $this->render($data, 'layout.' . $template, $path, $opts); } $layout->addToBody(function () use ($data, $template, $path, $opts) { return $this->render($data, 'layout.' . $template, $path, $opts); }); return $layout->render(); } /** * Get Layout instance * @return Layout */ public function getLayout() { if (! $this->layout) { $this->layout = new Layout(); } return $this->layout; } /** * Set Layout instance * @param Layout $layout * @return void */ public function setLayout($layout) { $this->layout = $layout; } /** * Устанавливаем layout * @param string $layoutTemplate название * @param array|null $settings * @return void * @noinspection PhpUnusedParameterInspection */ public function setLayoutTemplate(string $layoutTemplate = '', ?array $settings = null) { $this->layoutTemplate = $layoutTemplate; } /** * Получаем текущий layout * @return string */ public function getLayoutTemplate(): string { return $this->layoutTemplate; } /** * Подключаем javascript файл * @param string|array $file название скрипта(без расширения ".js") или полный URL * @param array|bool $opts [ * 'version' => версия подключаемого файла (для скриптов приложения) или FALSE * 'top' => подключить и переместить в начало списка * ] * @param bool * @return bool */ public function script($file, $opts = []): bool { if (empty($file)) { return false; } if (is_bool($opts)) { $opts = ['core' => $opts]; } elseif (is_string($opts) || is_numeric($opts)) { $opts = ['version' => $opts]; } elseif (!is_array($opts)) { $opts = []; } $opts = $this->defaults($opts, [ 'core' => $this->app->adminPanel(), 'attr' => [], 'version' => null, 'top' => false, ]); if (! is_array($file)) { $file = [$file]; } $list = []; foreach ($file as $js) { if (array_key_exists($js, $this->packagesList)) { $this->package($js); continue; } if (! $this->isUrl($js)) { if (mb_stripos($js, '/') !== 0) { $core = $opts['core']; if (mb_stripos($js, '@core/') === 0) { $core = true; $js = mb_substr($js, 6); } if ($core) { $opts['version'] = $opts['version'] ?? $this->manifestCoreVersion('/' . $js); } $js = ($core ? '/js/bff/' : '/js/') . $js; # name.js } $js = $js . (mb_substr($js, -3) !== '.js' ? '.js' : ''); $js = $this->app->url($js, $opts['version']); } if (! in_array($js, $this->scripts, true)) { $list[$js] = [ 'url' => $js, 'attr' => $opts['attr'], ]; } } if (empty($list)) { return false; } if ($opts['top']) { foreach ($this->scripts as $k => $v) { $list[$k] = $v; } $this->scripts = $list; } else { foreach ($list as $k => $v) { $this->scripts[$k] = $v; } } return true; } /** * Формирование списка подключаемых JavaScript файлов * @param array|null $opts: * bool html * bool hooks * bool minify * bool adminPanel * @return array|string */ public function scriptsRender(?array $opts = []) { if (is_null($opts)) { $list = $this->scripts; $this->scripts = []; return $list; } $opts = $this->defaults($opts, [ 'list' => [], 'html' => true, 'hooks' => true, 'minify' => false, 'adminPanel' => $this->app->adminPanel(), ]); if ($opts['hooks']) { # Extend scripts list $this->app->hook(($opts['adminPanel'] ? 'admin.' : '') . 'js.extra'); } $list = $this->scripts; if (! empty($opts['list'])) { $list = $opts['list']; } if ($opts['minify']) { Minifier::process($list); } $list = $this->app->filter(($opts['adminPanel'] ? 'admin.' : '') . 'js.includes', $list, $opts); foreach ($list as $k => $v) { if (! is_array($v)) { $list[$k] = ['url' => $v]; } } func::sortByPriority($list, 'priority'); if ($opts['html']) { $html = ''; foreach ($list as $v) { $attr = $this->defaults($v['attr'] ?? [], [ 'src' => $v['url'], 'type' => 'text/javascript', 'charset' => 'utf-8', ]); $html .= '' . PHP_EOL; } return $html; } return $list; } /** * Strip surrounding "; if ($reset) { unset($this->scriptsInline[$position]); } return $html; } /** * Set inline scripts default position * @param int|string|null $position * @return int previous position */ public function scriptsInlineDefaultPosition($position = null) { if ($position) { $before = $this->scriptsInlineDefaultPosition; $this->scriptsInlineDefaultPosition = $position; return $before; } return $this->scriptsInlineDefaultPosition; } /** * Подключаем CSS файл * @param string|array $file название css файла(без расширения ".css") или полный URL * @param array|string|int $opts доп. параметры [version=>mixed, top=>bool] * @return bool */ public function style($file, $opts = []) { if (empty($file)) { return false; } if (! is_array($file)) { $file = [$file]; } if (is_string($opts) || is_numeric($opts)) { $opts = ['version' => $opts]; } $opts = $this->defaults($opts, [ 'version' => null, 'top' => false, ]); $list = []; foreach ($file as $k => $v) { $name = (is_string($k) ? $k : $v); if (array_key_exists($name, $this->packagesList)) { $this->package($name); continue; } if (isset($this->styles[$name])) { continue; } $extension = (mb_substr($name, -4) !== '.css' ? '.css' : ''); if ($this->isUrl($name)) { $list[$name] = $name . $extension . (!empty($opts['version']) ? '?v=' . $opts['version'] : ''); } else { if (mb_stripos($name, '@core/') === 0) { $name = mb_substr($name, 6); $opts['version'] = $opts['version'] ?? $this->manifestCoreVersion('/' . $name); $name = '/js/bff/' . $name; } elseif (mb_stripos($name, '/') !== 0) { $name = '/css/' . $name; # name.css } $list[$name] = $this->app->url($name . $extension, $opts['version']); } } if (empty($list)) { return false; } if ($opts['top']) { foreach ($this->styles as $k => $v) { $list[$k] = $v; } $this->styles = $list; } else { foreach ($list as $k => $v) { $this->styles[$k] = $v; } } return true; } /** * Формирование списка подключаемых CSS файлов * @param array|null $opts: * array list * bool html * bool hooks * bool minify * bool adminPanel * @return string|array */ public function stylesRender(?array $opts = []) { if (is_null($opts)) { $list = $this->styles; $this->styles = []; return $list; } $opts = $this->defaults($opts, [ 'list' => [], 'html' => true, 'hooks' => true, 'minify' => false, 'adminPanel' => $this->app->adminPanel(), ]); $html = ''; if ($opts['hooks']) { # Extend styles list $this->start(); $this->app->hook(($opts['adminPanel'] ? 'admin.' : '') . 'css.extra'); # output allowed $html .= $this->stop(); } $list = $this->styles; if (! empty($opts['list'])) { $list = $opts['list']; } if ($opts['minify']) { Minifier::process($list); } $list = $this->app->filter(($opts['adminPanel'] ? 'admin.' : '') . 'css.includes', $list, $opts); foreach ($list as $k => $v) { if (! is_array($v)) { $list[$k] = ['url' => $v]; } } func::sortByPriority($list, 'priority'); if ($opts['html']) { foreach ($list as $v) { $attr = $this->defaults($v['attr'] ?? [], [ 'href' => $v['url'], 'type' => 'text/css', 'rel' => 'stylesheet', ]); $html .= '' . PHP_EOL; } return $html; } return $list; } /** * File manifest version * @param string $file * @param string $manifest file path * @param string $hot file path * @param array $options [hashOnly] * @return string */ public function manifestVersion(string $file, string $manifest, string $hot, array $options = []) { if (! array_key_exists($manifest, $this->manifestFiles)) { if ($this->files->exists($manifest)) { $this->manifestFiles[$manifest] = json_decode($this->files->get($manifest), true) ?? []; } } $hashOnly = $options['hashOnly'] ?? true; if (array_key_exists($file, $this->manifestFiles[$manifest] ?? [])) { if ($this->app->isDebug() && Request::cookie($this->app->cookieKey('hot'))) { if ($this->files->exists($hot)) { $hotUrl = rtrim($this->files->get($hot)); if ( mb_stripos($hotUrl, 'http://') === 0 || mb_stripos($hotUrl, 'https://') === 0 ) { $options['hotUrl'] = rtrim($hotUrl, '/'); } } } if ($hashOnly) { # Extract hash: "file?id=hash" => "hash" return explode('=', $this->manifestFiles[$manifest][$file])[1] ?? ''; } return $this->manifestFiles[$manifest][$file]; } if ($hashOnly) { return ''; } return $file; } /** * Core file manifest version * @param string $file * @param array $options [hashOnly] * @return string */ public function manifestCoreVersion(string $file, array $options = []) { return $this->manifestVersion( $file, $this->app->publicPath('/js/bff/' . static::MANIFEST_FILE), $this->app->publicPath('/js/bff/hot'), $options ); } /** * Проверяем является ли указанный путь корректным URL * @param string $path * @return bool */ public function isUrl(string $path): bool { if (! preg_match('~^(#|//|https?://)~', $path)) { return filter_var($path, FILTER_VALIDATE_URL) !== false; } return true; } /** * View registered inline block * @deprecated Use {@link View::block} instead * @param string $id * @param array $context * @return string */ public function tag(string $id, $context = []) { return $this->app->tags()->view($id, $context); } /** * View registered inline block * @param string $id block id or Block class * @param array $context view context data * @return string */ public function block(string $id, $context = []) { if (class_exists($id) && is_a($id, Block::class, true)) { return new $id($context); } return $this->app->tags()->view($id, $context); } /** * Reset state */ public function reset() { $this->pageData = []; $this->jsData = []; $this->sharedData = []; $this->layout = null; $this->setLayoutTemplate('main'); $this->packages = []; $this->scripts = []; $this->scriptsInline = []; $this->scriptsInlineOpen = []; $this->scriptsInlineDefaultPosition = self::POS_FOOT; $this->styles = []; $this->manifestFiles = []; } }