diff --git a/app/App/Providers/ThemeServiceProvider.php b/app/App/Providers/ThemeServiceProvider.php index 98ad509f3..e32f90b9a 100644 --- a/app/App/Providers/ThemeServiceProvider.php +++ b/app/App/Providers/ThemeServiceProvider.php @@ -4,9 +4,9 @@ namespace BookStack\App\Providers; use BookStack\Theming\ThemeEvents; use BookStack\Theming\ThemeService; +use BookStack\Theming\ThemeViews; use Illuminate\Support\Facades\Blade; use Illuminate\Support\ServiceProvider; -use Illuminate\View\View; class ThemeServiceProvider extends ServiceProvider { @@ -26,16 +26,21 @@ class ThemeServiceProvider extends ServiceProvider { // Boot up the theme system $themeService = $this->app->make(ThemeService::class); - $viewFactory = $this->app->make('view'); - $themeService->registerViewPathsForTheme($viewFactory->getFinder()); + if (!$themeService->getTheme()) { + return; + } - if ($themeService->logicalThemeIsActive()) { - $themeService->readThemeActions(); - $themeService->dispatch(ThemeEvents::APP_BOOT, $this->app); - $viewFactory->share('__theme', $themeService); + $themeService->readThemeActions(); + $themeService->dispatch(ThemeEvents::APP_BOOT, $this->app); + + $themeViews = new ThemeViews(); + $themeService->dispatch(ThemeEvents::THEME_REGISTER_VIEWS, $themeViews); + $themeViews->registerViewPathsForTheme($viewFactory->getFinder()); + if ($themeViews->hasRegisteredViews()) { + $viewFactory->share('__themeViews', $themeViews); Blade::directive('include', function ($expression) { - return "handleViewInclude({$expression}, array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1])); ?>"; + return "handleViewInclude({$expression}, array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1])); ?>"; }); } } diff --git a/app/Theming/ThemeEvents.php b/app/Theming/ThemeEvents.php index 44630acae..c6266b32b 100644 --- a/app/Theming/ThemeEvents.php +++ b/app/Theming/ThemeEvents.php @@ -134,6 +134,16 @@ class ThemeEvents */ const ROUTES_REGISTER_WEB_AUTH = 'routes_register_web_auth'; + + /** + * Theme register views event. + * Called by the theme system when a theme is active, so that custom view templates can be registered + * to be rendered in addition to existing app views. + * + * @param \BookStack\Theming\ThemeViews $themeViews + */ + const THEME_REGISTER_VIEWS = 'theme_register_views'; + /** * Web before middleware action. * Runs before the request is handled but after all other middleware apart from those diff --git a/app/Theming/ThemeService.php b/app/Theming/ThemeService.php index 0a6327af8..14281adca 100644 --- a/app/Theming/ThemeService.php +++ b/app/Theming/ThemeService.php @@ -16,16 +16,6 @@ class ThemeService */ protected array $listeners = []; - /** - * @var array> - */ - protected array $beforeViews = []; - - /** - * @var array> - */ - protected array $afterViews = []; - /** * Get the currently configured theme. * Returns an empty string if not configured. @@ -92,6 +82,10 @@ class ThemeService public function readThemeActions(): void { $themeActionsFile = theme_path('functions.php'); + if (!$themeActionsFile || !file_exists($themeActionsFile)) { + return; + } + try { require $themeActionsFile; } catch (\Error $exception) { @@ -99,24 +93,6 @@ class ThemeService } } - /** - * Check if a logical theme is active. - */ - public function logicalThemeIsActive(): bool - { - $themeActionsFile = theme_path('functions.php'); - return $themeActionsFile && file_exists($themeActionsFile); - } - - /** - * Register any extra paths for where we may expect views to be located - * with the provided FileViewFinder, to make custom views available for use. - */ - public function registerViewPathsForTheme(FileViewFinder $finder): void - { - $finder->prependLocation(theme_path()); - } - /** * @see SocialDriverManager::addSocialDriver */ @@ -125,63 +101,4 @@ class ThemeService $driverManager = app()->make(SocialDriverManager::class); $driverManager->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect); } - - /** - * Provide the response for a blade template view include. - */ - public function handleViewInclude(string $viewPath, array $data = []): string - { - $viewsContent = [ - ...$this->renderViewSets($this->beforeViews[$viewPath] ?? [], $data), - view()->make($viewPath, $data)->render(), - ...$this->renderViewSets($this->afterViews[$viewPath] ?? [], $data), - ]; - - return implode("\n", $viewsContent); - } - - /** - * Register a custom view to be rendered before the given target view is included in the template system. - */ - public function registerViewToRenderBefore(string $targetView, string $localView, int $priority = 50): void - { - $this->registerAdjacentView($this->beforeViews, $targetView, $localView, $priority); - } - - /** - * Register a custom view to be rendered after the given target view is included in the template system. - */ - public function registerViewToRenderAfter(string $targetView, string $localView, int $priority = 50): void - { - $this->registerAdjacentView($this->afterViews, $targetView, $localView, $priority); - } - - protected function registerAdjacentView(array &$location, string $targetView, string $localView, int $priority = 50): void - { - $viewPath = theme_path($localView . '.blade.php'); - if (!file_exists($viewPath)) { - throw new ThemeException("Expected registered view file at \"{$viewPath}\" does not exist"); - } - - if (!isset($location[$targetView])) { - $location[$targetView] = []; - } - $location[$targetView][$viewPath] = $priority; - } - - /** - * @param array $viewSet - * @return string[] - */ - protected function renderViewSets(array $viewSet, array $data): array - { - $paths = array_keys($viewSet); - usort($paths, function (string $a, string $b) use ($viewSet) { - return $viewSet[$a] <=> $viewSet[$b]; - }); - - return array_map(function (string $viewPath) use ($data) { - return view()->file($viewPath, $data)->render(); - }, $paths); - } } diff --git a/app/Theming/ThemeViews.php b/app/Theming/ThemeViews.php new file mode 100644 index 000000000..719f8e3ce --- /dev/null +++ b/app/Theming/ThemeViews.php @@ -0,0 +1,96 @@ +> + */ + protected array $beforeViews = []; + + /** + * @var array> + */ + protected array $afterViews = []; + + /** + * Register any extra paths for where we may expect views to be located + * with the provided FileViewFinder, to make custom views available for use. + */ + public function registerViewPathsForTheme(FileViewFinder $finder): void + { + $finder->prependLocation(theme_path()); + } + + /** + * Provide the response for a blade template view include. + */ + public function handleViewInclude(string $viewPath, array $data = []): string + { + if (!$this->hasRegisteredViews()) { + return view()->make($viewPath, $data)->render(); + } + + $viewsContent = [ + ...$this->renderViewSets($this->beforeViews[$viewPath] ?? [], $data), + view()->make($viewPath, $data)->render(), + ...$this->renderViewSets($this->afterViews[$viewPath] ?? [], $data), + ]; + + return implode("\n", $viewsContent); + } + + /** + * Register a custom view to be rendered before the given target view is included in the template system. + */ + public function renderBefore(string $targetView, string $localView, int $priority = 50): void + { + $this->registerAdjacentView($this->beforeViews, $targetView, $localView, $priority); + } + + /** + * Register a custom view to be rendered after the given target view is included in the template system. + */ + public function renderAfter(string $targetView, string $localView, int $priority = 50): void + { + $this->registerAdjacentView($this->afterViews, $targetView, $localView, $priority); + } + + public function hasRegisteredViews(): bool + { + return !empty($this->beforeViews) && !empty($this->afterViews); + } + + protected function registerAdjacentView(array &$location, string $targetView, string $localView, int $priority = 50): void + { + $viewPath = theme_path($localView . '.blade.php'); + if (!file_exists($viewPath)) { + throw new ThemeException("Expected registered view file at \"{$viewPath}\" does not exist"); + } + + if (!isset($location[$targetView])) { + $location[$targetView] = []; + } + $location[$targetView][$viewPath] = $priority; + } + + /** + * @param array $viewSet + * @return string[] + */ + protected function renderViewSets(array $viewSet, array $data): array + { + $paths = array_keys($viewSet); + usort($paths, function (string $a, string $b) use ($viewSet) { + return $viewSet[$a] <=> $viewSet[$b]; + }); + + return array_map(function (string $viewPath) use ($data) { + return view()->file($viewPath, $data)->render(); + }, $paths); + } +} diff --git a/tests/ThemeTest.php b/tests/ThemeTest.php index 014f3a92f..f640513cf 100644 --- a/tests/ThemeTest.php +++ b/tests/ThemeTest.php @@ -492,7 +492,7 @@ END; }); } - public function test_register_view_to_render_before_and_after() + public function test_theme_register_views_event_to_insert_views_before_and_after() { $this->usingThemeFolder(function (string $folder) { $before = 'this-is-my-before-header-string'; @@ -502,10 +502,14 @@ END; $functionsContent = <<<'CONTENT' renderBefore('layouts.parts.header', 'before', 4); + $themeViews->renderAfter('layouts.parts.header', 'after-a', 4); + $themeViews->renderAfter('layouts.parts.header', 'after-b', 1); + $themeViews->renderAfter('layouts.parts.header', 'after-c', 12); +}); CONTENT; $viewDir = theme_path(); @@ -516,12 +520,15 @@ CONTENT; file_put_contents($viewDir . '/after-c.blade.php', $afterC); $this->refreshApplication(); + $this->artisan('view:clear'); $resp = $this->get('/login'); $resp->assertSee($before); // Ensure ordering of the multiple after views $resp->assertSee($afterB . "\n" . $afterA . "\nthis-is-my-after-header-string-52"); }); + + $this->artisan('view:clear'); } protected function usingThemeFolder(callable $callback)