config = $config; $this->currentLanguage = $this->getCurrentLanguage(); $this->translations = $this->loadTranslations($this->currentLanguage); $this->buildMenu(); if (isset($_GET['search'])) { $this->performSearch($_GET['search']); } } /** * Get current language from URL or use default * * @return string Current language code */ private function getCurrentLanguage() { return $_GET['lang'] ?? $this->config['language']['default'] ?? 'nl'; } /** * Load translations for specified language * * @param string $lang Language code * @return array Translations array */ private function loadTranslations($lang) { $langFile = __DIR__ . '/../../lang/' . $lang . '.php'; error_log("Loading language file: " . $langFile); if (file_exists($langFile)) { $translations = include $langFile; error_log("Loaded translations for " . $lang . ": " . print_r($translations, true)); return $translations; } // Fallback to default language $defaultLang = $this->config['language']['default'] ?? 'nl'; $defaultLangFile = __DIR__ . '/../../lang/' . $defaultLang . '.php'; error_log("Fallback to default language: " . $defaultLangFile); return file_exists($defaultLangFile) ? include $defaultLangFile : []; } /** * Get translated text * * @param string $key Translation key * @return string Translated text */ public function t($key) { return $this->translations[$key] ?? $key; } /** * Build menu structure from content directory * * @return void */ private function buildMenu() { $this->menu = $this->scanDirectory($this->config['content_dir'], ''); } /** * Recursively scan directory for content files and folders * * @param string $dir Directory path to scan * @param string $prefix Relative path prefix * @return array Array of menu items */ private function scanDirectory($dir, $prefix) { if (!is_dir($dir)) return []; $items = scandir($dir); sort($items); $result = []; foreach ($items as $item) { if ($item[0] === '.') continue; // Skip language-specific content that doesn't match current language if (preg_match('/^(nl|en)\./', $item)) { $langPrefix = substr($item, 0, 2); if (($langPrefix === 'nl' && $this->currentLanguage !== 'nl') || ($langPrefix === 'en' && $this->currentLanguage !== 'en')) { continue; } } $path = $dir . '/' . $item; $relativePath = $prefix ? $prefix . '/' . $item : $item; if (is_dir($path)) { $result[] = [ 'type' => 'folder', 'title' => $this->formatDisplayName($item), 'path' => $relativePath, 'children' => $this->scanDirectory($path, $relativePath) ]; } elseif (preg_match('/\.(md|php|html)$/', $item)) { // Always use filename for navigation (not H1 titles from content) $filename = pathinfo($item, PATHINFO_FILENAME); $title = $this->formatDisplayName($filename); $pathWithoutExt = preg_replace('/\.[^.]+$/', '', $relativePath); $result[] = [ 'type' => 'file', 'title' => $title, 'path' => $pathWithoutExt, 'url' => '?page=' . $pathWithoutExt . '&lang=' . $this->currentLanguage ]; } } return $result; } /** * Perform search across all content files * * @param string $query Search query string * @return void */ private function performSearch($query) { $this->searchResults = []; $this->searchInDirectory($this->config['content_dir'], '', $query); } /** * Recursively search for query in directory files * * @param string $dir Directory to search in * @param string $prefix Relative path prefix * @param string $query Search query * @return void */ private function searchInDirectory($dir, $prefix, $query) { if (!is_dir($dir)) return; $items = scandir($dir); foreach ($items as $item) { if ($item[0] === '.') continue; $path = $dir . '/' . $item; $relativePath = $prefix ? $prefix . '/' . $item : $item; if (is_dir($path)) { $this->searchInDirectory($path, $relativePath, $query); } elseif (preg_match('/\.(md|php|html)$/', $item)) { $content = file_get_contents($path); if (stripos($content, $query) !== false || stripos($item, $query) !== false) { $title = ucfirst(pathinfo($item, PATHINFO_FILENAME)); $this->searchResults[] = [ 'title' => $title, 'path' => $relativePath, 'url' => '?page=' . $relativePath, 'snippet' => $this->createSnippet($content, $query) ]; } } } } /** * Create search snippet with highlighted query * * @param string $content Full content to create snippet from * @param string $query Search query to highlight * @return string Formatted snippet */ private function createSnippet($content, $query) { $content = strip_tags($content); $pos = stripos($content, $query); if ($pos === false) return substr($content, 0, 100) . '...'; $start = max(0, $pos - 50); $snippet = substr($content, $start, 150); return '...' . $snippet . '...'; } /** * Get current page content based on request * * @return array Page data with title and content */ public function getPage() { if (isset($_GET['search'])) { return $this->getSearchResults(); } // Check if guide is requested if (isset($_GET['guide'])) { return $this->getGuidePage(); } // Check if content directory is empty if ($this->isContentDirEmpty()) { return $this->getGuidePage(); } $page = $_GET['page'] ?? $this->config['default_page']; // Only remove file extension at the end, not all dots $pageWithoutExt = preg_replace('/\.(md|php|html)$/', '', $page); $filePath = $this->config['content_dir'] . '/' . $pageWithoutExt; // Check if directory exists FIRST (directories take precedence over files) if (is_dir($filePath)) { return $this->getDirectoryListing($pageWithoutExt, $filePath); } $actualFilePath = null; // Check if directory exists first (directories take precedence over files) if (is_dir($filePath)) { $directoryResult = $this->getDirectoryListing($pageWithoutExt, $filePath); return $directoryResult; } // Check for exact file matches if no directory found if (file_exists($filePath . '.md')) { $actualFilePath = $filePath . '.md'; $result = $this->parseMarkdown(file_get_contents($actualFilePath), $actualFilePath); } elseif (file_exists($filePath . '.php')) { $actualFilePath = $filePath . '.php'; $result = $this->parsePHP($actualFilePath); } elseif (file_exists($filePath . '.html')) { $actualFilePath = $filePath . '.html'; $result = $this->parseHTML(file_get_contents($actualFilePath)); } elseif (file_exists($filePath)) { $actualFilePath = $filePath; $extension = pathinfo($filePath, PATHINFO_EXTENSION); if ($extension === 'md') { $result = $this->parseMarkdown(file_get_contents($actualFilePath), $actualFilePath); } elseif ($extension === 'php') { $result = $this->parsePHP($actualFilePath); } elseif ($extension === 'html') { $result = $this->parseHTML(file_get_contents($actualFilePath)); } } // If no exact match found, check for language-specific versions if (!isset($result)) { $result = null; // Reset result before language-specific search $langPrefix = $this->currentLanguage; if (file_exists($this->config['content_dir'] . '/' . $langPrefix . '.' . $pageWithoutExt . '.md')) { $actualFilePath = $this->config['content_dir'] . '/' . $langPrefix . '.' . $pageWithoutExt . '.md'; $result = $this->parseMarkdown(file_get_contents($actualFilePath), $actualFilePath); } elseif (file_exists($this->config['content_dir'] . '/' . $langPrefix . '.' . $pageWithoutExt . '.php')) { $actualFilePath = $this->config['content_dir'] . '/' . $langPrefix . '.' . $pageWithoutExt . '.php'; $result = $this->parsePHP($actualFilePath); } elseif (file_exists($this->config['content_dir'] . '/' . $langPrefix . '.' . $pageWithoutExt . '.html')) { $actualFilePath = $this->config['content_dir'] . '/' . $langPrefix . '.' . $pageWithoutExt . '.html'; $result = $this->parseHTML(file_get_contents($actualFilePath)); } } // If no file found, check if it's a directory (directories take precedence) if (!isset($result) && is_dir($filePath)) { return $this->getDirectoryListing($pageWithoutExt, $filePath); } if (isset($result) && $actualFilePath) { $result['file_info'] = $this->getFileInfo($actualFilePath); return $result; } return $this->getError404(); } /** * Get file information including creation and modification dates * * @param string $filePath Path to the file * @return array|null File information or null if file doesn't exist */ private function getFileInfo($filePath) { if (!file_exists($filePath)) { return null; } $stats = stat($filePath); $created = date('d-m-Y H:i', $stats['ctime']); $modified = date('d-m-Y H:i', $stats['mtime']); return [ 'created' => $created, 'modified' => $modified, 'size' => $this->formatFileSize($stats['size']) ]; } /** * Format file size in human readable format * * @param int $bytes File size in bytes * @return string Formatted file size */ private function formatFileSize($bytes) { $units = ['B', 'KB', 'MB', 'GB']; $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= pow(1024, $pow); return round($bytes, 2) . ' ' . $units[$pow]; } /** * Get search results page content * * @return array Search results page data */ private function getSearchResults() { $query = $_GET['search']; $content = '

' . $this->t('search') . ' ' . $this->t('results_found') . ': "' . htmlspecialchars($query) . '"

'; if (empty($this->searchResults)) { $content .= '

' . $this->t('no_results') . '.

'; } else { $content .= '

' . count($this->searchResults) . ' ' . $this->t('results_found') . ':

'; foreach ($this->searchResults as $result) { $content .= '
'; $content .= '
'; $content .= '
' . htmlspecialchars($result['title']) . '
'; $content .= '

' . htmlspecialchars($result['path']) . '

'; $content .= '

' . htmlspecialchars($result['snippet']) . '

'; $content .= '
'; } } return [ 'title' => 'Search Results', 'content' => $content ]; } /** * Parse Markdown content to HTML using League CommonMark * * @param string $content Raw Markdown content * @return array Parsed content with title and body */ private function parseMarkdown($content, $actualFilePath = '') { // Extract title from first H1 $title = ''; if (preg_match('/^#\s+(.+)$/m', $content, $matches)) { $title = trim($matches[1]); } // Include autoloader require_once __DIR__ . '/../../../vendor/autoload.php'; // Configure CommonMark environment $config = [ 'html_input' => 'strip', 'allow_unsafe_links' => false, 'max_nesting_level' => 100, ]; // Create environment with extensions $environment = new \League\CommonMark\Environment\Environment($config); $environment->addExtension(new \League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension()); $environment->addExtension(new \League\CommonMark\Extension\Autolink\AutolinkExtension()); $environment->addExtension(new \League\CommonMark\Extension\Strikethrough\StrikethroughExtension()); $environment->addExtension(new \League\CommonMark\Extension\Table\TableExtension()); $environment->addExtension(new \League\CommonMark\Extension\TaskList\TaskListExtension()); // Create converter $converter = new \League\CommonMark\MarkdownConverter($environment); // Convert to HTML $body = $converter->convert($content)->getContent(); // Extract clean filename for title (without language prefix and extension) $filename = basename($actualFilePath); $cleanName = $this->formatDisplayName($filename); // Auto-link page titles to existing content pages (but not in H1 tags) $body = $this->autoLinkPageTitles($body, $cleanName); // Convert relative internal links to CMS format $body = preg_replace('/href="\/blog\/([^"]+)"/', 'href="?page=blog/$1"', $body); $body = preg_replace('/href="\/([^"]+)"/', 'href="?page=$1"', $body); return [ 'title' => $cleanName ?: 'Untitled', 'content' => $body ]; } /** * Auto-link page titles found in content * * @param string $content Content to process for auto-linking * @param string $excludeTitle Title to exclude from auto-linking (current page title) * @return string Content with auto-linked page titles */ private function autoLinkPageTitles($content, $excludeTitle = '') { // Get all available pages with their titles $pages = $this->getAllPageTitles(); foreach ($pages as $pagePath => $pageTitle) { // Create a pattern that matches the exact page title (case-insensitive) // Use word boundaries and avoid H1 tags to prevent linking inside headings $pattern = '/\b' . preg_quote($pageTitle, '/') . '\b(?!(?=<\/h1>))/i'; // Replace with link, but avoid linking inside existing links, headings, or markdown $replacement = function($matches) use ($pageTitle, $pagePath, $excludeTitle) { $text = $matches[0]; // Check if we're inside an existing link or markdown syntax, or if it's the current page title if (preg_match('/\[.*?\]\(.*?\)/', $text) || preg_match('/\[.*?\]:/', $text) || preg_match('/]*>/', $text) || preg_match('/href=/', $text) || preg_match('/

/', $text) || strtolower($text) === strtolower($excludeTitle)) { return $text; // Don't link existing links, current page title, or H1 headings } return '' . $text . ''; }; $content = preg_replace_callback($pattern, $replacement, $content); } return $content; } /** * Get all page titles from content directory * * @return array Associative array of page paths to titles */ private function getAllPageTitles() { $pages = []; $this->scanForPageTitles($this->config['content_dir'], '', $pages); return $pages; } /** * Get all page names from content directory (for navigation) * * @return array Associative array of page paths to display names */ private function getAllPageNames() { $pages = []; $this->scanForPageNames($this->config['content_dir'], '', $pages); return $pages; } /** * Recursively scan for page titles in directory * * @param string $dir Directory to scan * @param string $prefix Relative path prefix * @param array &$pages Reference to pages array to populate * @return void */ private function scanForPageTitles($dir, $prefix, &$pages) { if (!is_dir($dir)) return; $items = scandir($dir); sort($items); foreach ($items as $item) { if ($item[0] === '.') continue; $path = $dir . '/' . $item; $relativePath = $prefix ? $prefix . '/' . $item : $item; if (is_dir($path)) { $this->scanForPageTitles($path, $relativePath, $pages); } elseif (preg_match('/\.(md|php|html)$/', $item)) { $title = $this->extractPageTitle($path); if ($title && !empty(trim($title))) { $pagePath = preg_replace('/\.[^.]+$/', '', $relativePath); $pages[$pagePath] = $title; } else { // Fallback to clean filename if no title found in content $filename = basename($path, pathinfo($path, PATHINFO_EXTENSION)); $cleanName = $this->formatDisplayName($filename); $pagePath = preg_replace('/\.[^.]+$/', '', $relativePath); $pages[$pagePath] = $cleanName; } } } } /** * Recursively scan for page names in directory (for navigation) * * @param string $dir Directory to scan * @param string $prefix Relative path prefix * @param array &$pages Reference to pages array to populate * @return void */ private function scanForPageNames($dir, $prefix, &$pages) { if (!is_dir($dir)) return; $items = scandir($dir); sort($items); foreach ($items as $item) { if ($item[0] === '.') continue; $path = $dir . '/' . $item; $relativePath = $prefix ? $prefix . '/' . $item : $item; if (is_dir($path)) { $this->scanForPageNames($path, $relativePath, $pages); } elseif (preg_match('/\.(md|php|html)$/', $item)) { // Use filename without extension as display name $displayName = preg_replace('/\.[^.]+$/', '', $item); $pagePath = preg_replace('/\.[^.]+$/', '', $relativePath); $pages[$pagePath] = $this->formatDisplayName($displayName); } } } /** * Format display name from filename * * @param string $filename Filename without extension * @return string Formatted display name */ private function formatDisplayName($filename) { // Debug: log input error_log("DEBUG: formatDisplayName input: '$filename'"); // Remove language prefixes (nl. or en.) from display names if (preg_match('/^(nl|en)\.(.+)$/', $filename, $matches)) { $filename = $matches[2]; } // Remove language prefixes from directory names (nl.php-testen -> php-testen) if (preg_match('/^(nl|en)\.php-(.+)$/', $filename, $matches)) { $filename = 'php-' . $matches[2]; } // Remove file extensions (.md, .php, .html) from display names $filename = preg_replace('/\.(md|php|html)$/', '', $filename); // Handle special cases first (only for exact filenames, not directories) // These should only apply to actual files, not directory names if (strtolower($filename) === 'phpinfo' && !preg_match('/\//', $filename)) { return 'phpinfo'; } if (strtolower($filename) === 'ict' && !preg_match('/\//', $filename)) { return 'ICT'; } // Replace hyphens and underscores with spaces $name = str_replace(['-', '_'], ' ', $filename); // Convert to title case (first letter uppercase, rest lowercase) $name = ucwords(strtolower($name)); // Handle other special cases $name = str_replace('Phpinfo', 'phpinfo', $name); $name = str_replace('Ict', 'ICT', $name); return $name; } /** * Extract page title from file content * * @param string $filePath Path to the file * @return string|null Extracted title or null if not found */ private function extractPageTitle($filePath) { $content = file_get_contents($filePath); $extension = pathinfo($filePath, PATHINFO_EXTENSION); if ($extension === 'md') { // Extract first H1 from Markdown if (preg_match('/^#\s+(.+)$/m', $content, $matches)) { return trim($matches[1]); } } elseif ($extension === 'php') { // Extract title from PHP file if (preg_match('/\$title\s*=\s*["\']([^"\']+)["\']/', $content, $matches)) { return trim($matches[1]); } } elseif ($extension === 'html') { // Extract title from HTML file if (preg_match('/(.*?)<\/title>/i', $content, $matches)) { return trim(strip_tags($matches[1])); } if (preg_match('/<h1[^>]*>(.*?)<\/h1>/i', $content, $matches)) { return trim(strip_tags($matches[1])); } } return null; } /** * Parse PHP file and capture output * * @param string $filePath Path to PHP file * @return array Parsed content with title and body */ private function parsePHP($filePath) { ob_start(); include $filePath; $content = ob_get_clean(); // Extract filename for title $filename = basename($filePath); $cleanName = $this->formatDisplayName($filename); return [ 'title' => $cleanName ?: 'Untitled', 'content' => $content ]; } /** * Parse HTML content and extract title * * @param string $content Raw HTML content * @return array Parsed content with title and body */ private function parseHTML($content, $actualFilePath = '') { // Extract filename for title $filename = basename($actualFilePath); $cleanName = $this->formatDisplayName($filename); return [ 'title' => $cleanName ?: 'Untitled', 'content' => $content ]; } /** * Check if content directory is empty * * @return bool True if content directory is empty or doesn't exist */ private function isContentDirEmpty() { $contentDir = $this->config['content_dir']; if (!is_dir($contentDir)) { return true; } $files = scandir($contentDir); $files = array_diff($files, ['.', '..']); return empty($files); } /** * Get guide page content based on user language * * @return array Guide page data */ private function getGuidePage() { $lang = $this->currentLanguage; $guideFile = __DIR__ . '/../../../guide/' . $lang . '.codepress.md'; if (!file_exists($guideFile)) { $guideFile = __DIR__ . '/../../../guide/en.codepress.md'; // Fallback to English } $content = file_get_contents($guideFile); $result = $this->parseMarkdown($content); // Set special title for guide $result['title'] = $this->t('manual') . ' - CodePress CMS'; return $result; } /** * Detect user language from browser headers * * @return string Language code ('nl' or 'en') */ private function detectLanguage() { // Simple language detection based on browser Accept-Language header $acceptLanguage = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''; if (strpos($acceptLanguage, 'nl') !== false) { return 'nl'; } return 'en'; // Default to English } /** * Generate directory listing page * * @param string $pagePath Relative path to directory * @param string $dirPath Absolute path to directory * @return array Directory listing page data */ private function getDirectoryListing($pagePath, $dirPath) { // Get the directory name from the path, not from a potential file $pathParts = explode('/', $pagePath); $dirName = end($pathParts); // Debug: log what we're working with error_log("DEBUG: getDirectoryListing - dirName: '$dirName', formatDisplayName result: '" . $this->formatDisplayName($dirName) . "'"); $title = $this->formatDisplayName($dirName) ?: 'Home'; $content = '<h1>' . htmlspecialchars($title) . '</h1>'; $result = [ 'title' => $title, 'content' => $content ]; // Debug: ensure we're returning the right title error_log("DEBUG: getDirectoryListing returning title: '$title'"); if (!is_dir($dirPath)) { return [ 'title' => $title, 'content' => $content . '<p>Directory not found.</p>' ]; } $items = scandir($dirPath); sort($items); $hasContent = false; // Collect all items $allItems = []; foreach ($items as $item) { if ($item[0] === '.') continue; $itemPath = $dirPath . '/' . $item; $relativePath = $pagePath ? $pagePath . '/' . $item : $item; if (is_dir($itemPath)) { $allItems[] = [ 'name' => ucfirst($item), 'path' => $relativePath, 'url' => '?page=' . $relativePath, 'icon' => 'bi-folder', 'type' => 'directory' ]; } elseif (preg_match('/\.(md|php|html)$/', $item)) { $extractedTitle = $this->extractPageTitle($itemPath); $fileTitle = $extractedTitle ?: ucfirst(pathinfo($item, PATHINFO_FILENAME)); $pathWithoutExt = preg_replace('/\.[^.]+$/', '', $relativePath); $icon = pathinfo($item, PATHINFO_EXTENSION) === 'md' ? 'bi-file-text' : (pathinfo($item, PATHINFO_EXTENSION) === 'php' ? 'bi-file-code' : 'bi-file-earmark'); $allItems[] = [ 'name' => $fileTitle, 'path' => $pathWithoutExt, 'url' => '?page=' . $pathWithoutExt, 'icon' => $icon, 'type' => 'file' ]; } } // Display all items in a single column if (!empty($allItems)) { $content .= '<div class="list-group">'; foreach ($allItems as $item) { $content .= '<a href="' . htmlspecialchars($item['url']) . '" class="list-group-item list-group-item-action d-flex align-items-center">'; $content .= '<i class="bi ' . $item['icon'] . ' me-3"></i>'; $content .= '<span>' . htmlspecialchars($item['name']) . '</span>'; $content .= '</a>'; } $content .= '</div>'; $hasContent = true; } $content .= '</div>'; if (!$hasContent) { $content .= '<p>' . $this->t('directory_empty') . '.</p>'; } return [ 'title' => $title, 'content' => $content ]; } /** * Get 404 error page content * * @return array 404 page data */ private function getError404() { return [ 'title' => $this->t('page_not_found'), 'content' => '<h1>404 - ' . $this->t('page_not_found') . '</h1><p>' . $this->t('page_not_found_text') . '</p>' ]; } /** * Get menu structure * * @return array Menu structure for navigation */ public function getMenu() { return $this->menu; } /** * Render the complete page with template * * @return void */ public function render() { $page = $this->getPage(); $menu = $this->getMenu(); $breadcrumb = $this->generateBreadcrumb(); // Get homepage title $homepageTitle = $this->getHomepageTitle(); // Prepare template data $templateData = [ 'site_title' => $this->config['site_title'], 'page_title' => htmlspecialchars($page['title']), 'content' => $page['content'], 'search_query' => isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '', 'menu' => $this->renderMenu($menu), 'breadcrumb' => $breadcrumb, 'default_page' => $this->config['default_page'], 'homepage' => $this->config['default_page'], 'homepage_title' => $homepageTitle, 'is_homepage' => (!isset($_GET['page']) || $_GET['page'] === $this->config['default_page']), 'home_active_class' => (!isset($_GET['page']) || $_GET['page'] === $this->config['default_page']) ? 'active' : '', 'is_guide_page' => isset($_GET['guide']), 'lang_switch_url' => isset($_GET['guide']) ? '&guide' : '&page=' . $this->config['default_page'], 'author_name' => $this->config['author']['name'] ?? 'CodePress Developer', 'author_website' => $this->config['author']['website'] ?? '#', 'author_git' => $this->config['author']['git'] ?? '#', 'seo_description' => $this->config['seo']['description'] ?? 'CodePress CMS - Lightweight file-based content management system', 'seo_keywords' => $this->config['seo']['keywords'] ?? 'cms, php, content management, file-based', // Theme colors 'header_color' => $this->config['theme']['header_color'] ?? '#0d6efd', 'header_font_color' => $this->config['theme']['header_font_color'] ?? '#ffffff', 'navigation_color' => $this->config['theme']['navigation_color'] ?? '#f8f9fa', 'navigation_font_color' => $this->config['theme']['navigation_font_color'] ?? '#000000', // Language 'current_lang' => $this->currentLanguage, 'current_lang_upper' => strtoupper($this->currentLanguage), 'available_langs' => $this->config['language']['available'] ?? ['nl', 'en'], // Translations 't_home' => $this->t('home'), 't_search' => $this->t('search'), 't_search_placeholder' => $this->t('search_placeholder'), 't_search_button' => $this->t('search_button'), 't_welcome' => $this->t('welcome'), 't_created' => $this->t('created'), 't_modified' => $this->t('modified'), 't_author' => $this->t('author'), 't_manual' => $this->t('manual'), 't_no_content' => $this->t('no_content'), 't_no_results' => $this->t('no_results'), 't_results_found' => $this->t('results_found'), 't_breadcrumb_home' => $this->t('breadcrumb_home'), 't_file_details' => $this->t('file_details'), 't_guide' => $this->t('guide'), 't_powered_by' => $this->t('powered_by'), 't_directory_empty' => $this->t('directory_empty'), 't_page_not_found' => $this->t('page_not_found'), 't_page_not_found_text' => $this->t('page_not_found_text'), 't_mappen' => $this->t('mappen'), 't_paginas' => $this->t('paginas') ]; // File info for footer if (isset($page['file_info'])) { $templateData['file_info'] = $this->t('created') . ': ' . htmlspecialchars($page['file_info']['created']) . ' | ' . $this->t('modified') . ': ' . htmlspecialchars($page['file_info']['modified']); $templateData['file_info_block'] = '<span class="file-details"> | ' . $templateData['file_info'] . '</span>'; } else { $templateData['file_info'] = ''; $templateData['file_info_block'] = ''; } // Check if content exists for guide link $hasContent = !$this->isContentDirEmpty(); $templateData['has_content'] = $hasContent; // Don't show site title link on guide page $templateData['show_site_link'] = !$this->isContentDirEmpty() && !isset($_GET['guide']); // Load and render all templates with data $layoutTemplate = file_get_contents($this->config['templates_dir'] . '/layout.mustache'); $headerTemplate = file_get_contents($this->config['templates_dir'] . '/assets/header.mustache'); $navigationTemplate = file_get_contents($this->config['templates_dir'] . '/assets/navigation.mustache'); $footerTemplate = file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache'); // Determine content type and load appropriate template $contentType = $this->getContentType($page); $contentTemplateFile = $this->config['templates_dir'] . '/' . $contentType . '_content.mustache'; $contentTemplate = file_exists($contentTemplateFile) ? file_get_contents($contentTemplateFile) : '<div class="content">{{{content}}}</div>'; // Render all templates with data $renderedHeader = SimpleTemplate::render($headerTemplate, $templateData); $renderedNavigation = SimpleTemplate::render($navigationTemplate, $templateData); $renderedFooter = SimpleTemplate::render($footerTemplate, $templateData); $renderedContent = SimpleTemplate::render($contentTemplate, $templateData); // Replace partials in layout $finalTemplate = str_replace('{{>header}}', $renderedHeader, $layoutTemplate); $finalTemplate = str_replace('{{>navigation}}', $renderedNavigation, $finalTemplate); $finalTemplate = str_replace('{{>footer}}', $renderedFooter, $finalTemplate); $finalTemplate = str_replace('{{>content_template}}', $renderedContent, $finalTemplate); // Render the final layout with all template data $renderedLayout = SimpleTemplate::render($finalTemplate, $templateData); echo $renderedLayout; } /** * Generate breadcrumb navigation HTML * * @return string Breadcrumb HTML */ private function generateBreadcrumb() { if (isset($_GET['search'])) { return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '&lang=' . $this->currentLanguage . '"></a></li><li class="breadcrumb-item"> > </li><li class="breadcrumb-item active">' . $this->t('search') . '</li></ol></nav>'; } $page = $_GET['page'] ?? $this->config['default_page']; $page = preg_replace('/\.[^.]+$/', '', $page); if ($page === $this->config['default_page']) { return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item active"><i class="bi bi-house"></i></li></ol></nav>'; } $breadcrumb = '<nav aria-label="breadcrumb"><ol class="breadcrumb">'; // Start with home icon linking to default page (root) $breadcrumb .= '<li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '&lang=' . $this->currentLanguage . '"><i class="bi bi-house"></i></a></li>'; // Split page path and build breadcrumb items $parts = explode('/', $page); $currentPath = ''; foreach ($parts as $i => $part) { $currentPath .= ($currentPath ? '/' : '') . $part; $title = ucfirst($part); if ($i === count($parts) - 1) { // Last part - active page $breadcrumb .= '<li class="breadcrumb-item"> > </li><li class="breadcrumb-item active">' . $title . '</li>'; } else { // Parent directory - clickable link with separator $breadcrumb .= '<li class="breadcrumb-item"> > </li><li class="breadcrumb-item"><a href="?page=' . $currentPath . '&lang=' . $this->currentLanguage . '">' . $title . '</a></li>'; } } $breadcrumb .= '</ol></nav>'; return $breadcrumb; } /** * Render menu HTML with dropdown support * * @param array $items Menu items to render * @param int $level Current nesting level * @return string Rendered menu HTML */ private function renderMenu($items, $level = 0) { $html = ''; foreach ($items as $item) { if ($item['type'] === 'folder') { $hasChildren = !empty($item['children']); if ($hasChildren) { $folderId = 'folder-' . str_replace('/', '-', $item['path']); $isExpanded = $this->folderContainsActivePage($item['children']); if ($level === 0) { // Root level folders $html .= '<li class="nav-item dropdown">'; $html .= '<a class="nav-link dropdown-toggle" href="#" id="' . $folderId . '" role="button" data-bs-toggle="dropdown" aria-expanded="' . ($isExpanded ? 'true' : 'false') . '">'; $html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>'; $html .= '</a>'; $html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">'; $html .= $this->renderMenu($item['children'], $level + 1); $html .= '</ul>'; $html .= '</li>'; } else { // Nested folders in dropdown $html .= '<li class="dropdown-submenu">'; $html .= '<a class="dropdown-item dropdown-toggle" href="#" id="' . $folderId . '">'; $html .= htmlspecialchars($item['title']) . ' <i class="bi bi-chevron-down"></i>'; $html .= '</a>'; $html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">'; $html .= $this->renderMenu($item['children'], $level + 1); $html .= '</ul>'; $html .= '</li>'; } } else { if ($level === 0) { $html .= '<li class="nav-item">'; $html .= '<span class="nav-link text-muted">' . htmlspecialchars($item['title']) . '</span>'; $html .= '</li>'; } else { $html .= '<li><span class="dropdown-item text-muted">' . htmlspecialchars($item['title']) . '</span></li>'; } } } else { // Show files in root as tabs, files in folders as dropdown items if ($level === 0) { // Don't show the homepage file as a separate tab since it's already the Home button if ($item['path'] === $this->config['default_page']) { continue; } $active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : ''; $html .= '<li class="nav-item">'; $html .= '<a class="nav-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">'; $html .= htmlspecialchars($item['title']); $html .= '</a>'; $html .= '</li>'; } else { // Show files in dropdown menus $active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : ''; $html .= '<li><a class="dropdown-item ' . $active . '" href="' . htmlspecialchars($item['url']) . '">'; $html .= htmlspecialchars($item['title']); $html .= '</a></li>'; } } } return $html; } /** * Determine content type for current page * * @param array $page Page data * @return string Content type (markdown, php, html) */ private function getContentType($page) { // Try to determine content type from page request $pagePath = $_GET['page'] ?? $this->config['default_page']; $pagePath = preg_replace('/\.[^.]+$/', '', $pagePath); $filePath = $this->config['content_dir'] . '/' . $pagePath; // Check for different file extensions if (file_exists($filePath . '.md')) { return 'markdown'; } elseif (file_exists($filePath . '.php')) { return 'php'; } elseif (file_exists($filePath . '.html')) { return 'html'; } elseif (file_exists($filePath)) { $extension = pathinfo($filePath, PATHINFO_EXTENSION); return in_array($extension, ['md', 'php', 'html']) ? $extension : 'markdown'; } // Default to markdown return 'markdown'; } /** * Get homepage title * * @return string Homepage title */ private function getHomepageTitle() { // Use formatted filename for homepage title in navigation return $this->formatDisplayName($this->config['default_page']); } /** * Check if folder contains the currently active page * * @param array $children Array of child items * @return bool True if active page is found in children */ private function folderContainsActivePage($children) { foreach ($children as $child) { if ($child['type'] === 'folder') { if (!empty($child['children']) && $this->folderContainsActivePage($child['children'])) { return true; } } else { if (isset($_GET['page']) && $_GET['page'] === $child['path']) { return true; } } } return false; } }