config = $config; $this->themeDir = $config['theme_dir'] ?? (__DIR__ . '/../../../themes/' . ($config['active_theme'] ?? 'default')); $this->themeConfig = $config['theme'] ?? []; $this->compiledCssDir = __DIR__ . '/../../../public/themes'; $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). * * @return string|null Absolute path to the compiled CSS, or null if none */ public function compileCss(): ?string { $scssFile = $this->themeDir . '/css/theme.scss'; if (!is_file($scssFile)) { return null; } $themeName = basename($this->themeDir); $outDir = $this->compiledCssDir . '/' . $themeName; $outFile = $outDir . '/theme.css'; $cacheFile = $outDir . '/.mtime'; $mtime = filemtime($scssFile); if (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 . '/css'); $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 compiled theme CSS, or null if unavailable. */ public function getCssUrl(): ?string { $compiled = $this->compileCss(); if ($compiled === null) { return null; } return '/themes/' . basename($this->themeDir) . '/theme.css'; } /** * Get the public URL for the theme JS, or null if unavailable. */ public function getJsUrl(): ?string { $jsFile = $this->themeDir . '/js/theme.js'; if (!is_file($jsFile)) { return null; } return '/themes/' . basename($this->themeDir) . '/js/theme.js'; } }