config = $config; $this->themeDir = $config['theme_dir'] ?? (__DIR__ . '/../../../themes/' . ($config['active_theme'] ?? 'default')); $this->themeConfig = $config['theme'] ?? []; $loader = new FilesystemLoader($this->themeDir); $this->twig = new Environment($loader, [ 'cache' => false, 'autoescape' => false, ]); } /** * Get the absolute path of the active theme directory */ public function getThemeDir(): string { return $this->themeDir; } /** * Get the raw theme.json config array */ public function getThemeConfig(): array { return $this->themeConfig; } /** * Get the theme title (from theme.json 'title' or 'name') */ public function getTitle(): string { return $this->themeConfig['title'] ?? $this->themeConfig['name'] ?? basename($this->themeDir); } /** * Get the theme config section (default_template, background settings, etc.) */ public function getConfig(): array { return $this->themeConfig['config'] ?? []; } /** * Get the theme template section (layout key => .twig file mapping) */ public function getTemplates(): array { return $this->themeConfig['template'] ?? []; } /** * Resolve the .twig template file for a requested layout. * * Templates are defined in theme.json under the "template" section: * { "template": { "full_content": "full_content.twig", ... } } * The default template is defined in the "config" section: * { "config": { "default_template": "full_content", ... } } * * Priority: * 1. If the layout is a known key in the "template" section, use its mapped file. * 2. Otherwise fall back to config.default_template. * 3. Final safety net: full_content.twig. * * @param string $layout Requested layout key (e.g. 'left_sidebar') * @return string Template name usable by the Twig loader */ public function getTemplateForLayout(string $layout): string { $layout = trim($layout); $templates = $this->getTemplates(); if ($layout !== '' && isset($templates[$layout])) { $file = $templates[$layout]; if ($this->templateExists($file)) { return $file; } } $config = $this->getConfig(); $default = $config['default_template'] ?? 'full_content'; if (isset($templates[$default])) { $file = $templates[$default]; if ($this->templateExists($file)) { return $file; } } return 'full_content.twig'; } /** * Get the list of available layout keys defined in the "template" section. * * @return array List of layout keys */ public function getLayouts(): array { return array_keys($this->getTemplates()); } /** * Check whether a template file exists in the theme directory */ private function templateExists(string $file): bool { $path = $this->themeDir . '/' . ltrim($file, '/'); return is_file($path); } /** * Render a layout template with the given data. * * @param string $layout Requested layout key * @param array $data Template variables * @return string Rendered HTML */ public function render(string $layout, array $data): string { $template = $this->getTemplateForLayout($layout); return $this->twig->render($template, $data); } /** * Compile the theme's SCSS to CSS (cached by source mtime). * Compiles from assets/scss/theme.scss to assets/css_compiled/theme.css * * @param bool $force Force recompilation * @return string|null Absolute path to the compiled CSS, or null if none */ public function compileCss(bool $force = false): ?string { $scssFile = $this->themeDir . '/assets/scss/theme.scss'; if (!is_file($scssFile)) { return null; } $outDir = $this->themeDir . '/assets/css_compiled'; $outFile = $outDir . '/theme.css'; $cacheFile = $outDir . '/.mtime'; $mtime = filemtime($scssFile); if (!$force && is_file($outFile) && is_file($cacheFile) && (int)file_get_contents($cacheFile) === $mtime) { return $outFile; } if (!is_dir($outDir)) { mkdir($outDir, 0755, true); } try { $compiler = new Compiler(); $compiler->setImportPaths($this->themeDir . '/assets/scss'); $css = $compiler->compileString(file_get_contents($scssFile))->getCss(); file_put_contents($outFile, $css); file_put_contents($cacheFile, (string)$mtime); return $outFile; } catch (\Throwable $e) { error_log('ThemeManager SCSS compile error: ' . $e->getMessage()); return null; } } /** * Get the public URL for the theme CSS. * Priority: 1) assets/css/theme.css (manual), 2) assets/css_compiled/theme.css (compiled), 3) null */ public function getCssUrl(): ?string { $manualCss = $this->themeDir . '/assets/css/theme.css'; $compiledCss = $this->themeDir . '/assets/css_compiled/theme.css'; if (is_file($manualCss)) { return $this->getThemeAssetUrl('css/theme.css'); } if (is_file($compiledCss)) { return $this->getThemeAssetUrl('css_compiled/theme.css'); } $compiled = $this->compileCss(); if ($compiled !== null) { return $this->getThemeAssetUrl('css_compiled/theme.css'); } return null; } /** * Get the public URL for the theme JS, or null if unavailable. */ public function getJsUrl(): ?string { $jsFile = $this->themeDir . '/assets/js/theme.js'; if (!is_file($jsFile)) { return null; } return $this->getThemeAssetUrl('js/theme.js'); } /** * Get public URL for a theme asset. */ private function getThemeAssetUrl(string $assetPath): string { $themeName = basename($this->themeDir); return '/themes/' . $themeName . '/assets/' . $assetPath; } /** * Check if theme has SCSS source file. */ public function hasScss(): bool { return is_file($this->themeDir . '/assets/scss/theme.scss'); } /** * Check if compiled CSS is newer than SCSS source. */ public function isScssCompiled(): bool { $scssFile = $this->themeDir . '/assets/scss/theme.scss'; $cssFile = $this->themeDir . '/assets/css_compiled/theme.css'; if (!is_file($scssFile) || !is_file($cssFile)) { return false; } return filemtime($cssFile) >= filemtime($scssFile); } /** * Check if theme has manual CSS file. */ public function hasManualCss(): bool { return is_file($this->themeDir . '/assets/css/theme.css'); } /** * Get list of CSS files in theme assets/css/ directory. * Excludes css_compiled directory. */ public function getCssFiles(): array { $cssDir = $this->themeDir . '/assets/css'; $files = []; if (!is_dir($cssDir)) { return $files; } foreach (scandir($cssDir) as $file) { if ($file[0] === '.') continue; if (pathinfo($file, PATHINFO_EXTENSION) === 'css') { $files[] = $this->getThemeAssetUrl('css/' . $file); } } return $files; } /** * Get list of JS files in theme assets/js/ directory. */ public function getJsFiles(): array { $jsDir = $this->themeDir . '/assets/js'; $files = []; if (!is_dir($jsDir)) { return $files; } foreach (scandir($jsDir) as $file) { if ($file[0] === '.') continue; if (pathinfo($file, PATHINFO_EXTENSION) === 'js') { $files[] = $this->getThemeAssetUrl('js/' . $file); } } return $files; } /** * Get URL for favicon if it exists. */ public function getFaviconUrl(): ?string { $faviconFile = $this->themeDir . '/assets/img/favicon.svg'; if (!is_file($faviconFile)) { return null; } return $this->getThemeAssetUrl('img/favicon.svg'); } }