diff --git a/app/App/Providers/ThemeServiceProvider.php b/app/App/Providers/ThemeServiceProvider.php index a806c1df6..98ad509f3 100644 --- a/app/App/Providers/ThemeServiceProvider.php +++ b/app/App/Providers/ThemeServiceProvider.php @@ -4,7 +4,9 @@ namespace BookStack\App\Providers; use BookStack\Theming\ThemeEvents; use BookStack\Theming\ThemeService; +use Illuminate\Support\Facades\Blade; use Illuminate\Support\ServiceProvider; +use Illuminate\View\View; class ThemeServiceProvider extends ServiceProvider { @@ -24,8 +26,17 @@ class ThemeServiceProvider extends ServiceProvider { // Boot up the theme system $themeService = $this->app->make(ThemeService::class); - $themeService->registerViewPathsForTheme($this->app->make('view')->getFinder()); - $themeService->readThemeActions(); - $themeService->dispatch(ThemeEvents::APP_BOOT, $this->app); + + $viewFactory = $this->app->make('view'); + $themeService->registerViewPathsForTheme($viewFactory->getFinder()); + + if ($themeService->logicalThemeIsActive()) { + $themeService->readThemeActions(); + $themeService->dispatch(ThemeEvents::APP_BOOT, $this->app); + $viewFactory->share('__theme', $themeService); + Blade::directive('include', function ($expression) { + return "handleViewInclude({$expression}, array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1])); ?>"; + }); + } } } diff --git a/app/Theming/ThemeService.php b/app/Theming/ThemeService.php index 87811f0ef..9587ceccb 100644 --- a/app/Theming/ThemeService.php +++ b/app/Theming/ThemeService.php @@ -16,6 +16,16 @@ 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. @@ -82,15 +92,22 @@ class ThemeService public function readThemeActions(): void { $themeActionsFile = theme_path('functions.php'); - if ($themeActionsFile && file_exists($themeActionsFile)) { - try { - require $themeActionsFile; - } catch (\Error $exception) { - throw new ThemeException("Failed loading theme functions file at \"{$themeActionsFile}\" with error: {$exception->getMessage()}"); - } + try { + require $themeActionsFile; + } catch (\Error $exception) { + throw new ThemeException("Failed loading theme functions file at \"{$themeActionsFile}\" with error: {$exception->getMessage()}"); } } + /** + * 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. @@ -108,4 +125,63 @@ 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 registerViewRenderBefore(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 registerViewRenderAfter(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); + } }