config = [ 'title' => 'Navigatie', 'viewable' => true, 'type' => 'content', ]; } public function setAPI(CMSAPI $api): void { $this->api = $api; } public function getConfig(): array { return $this->config; } public function setConfig(array $config): void { $this->config = array_merge($this->config, $config); } /** * Get the path to this plugin's CSS file (relative to web root). */ public function getCssUrl(): string { return '/plugins/Navigation/assets/css/navigation.css'; } /** * Generate sidebar navigation. * Detects whether we're on a guide page or content page and builds nav accordingly. */ public function getSidebarContent(): string { $isGuide = isset($_GET['guide']) || ($_GET['page'] ?? '') === 'guide'; $isAdminGuide = isset($_GET['route']) && $_GET['route'] === 'guide'; $isAdmin = isset($_GET['route']); $lang = $_GET['lang'] ?? ($this->api ? $this->api->getCurrentLanguage() : 'nl'); $currentPage = $_GET['page'] ?? ''; if ($isGuide || $isAdminGuide) { return $this->buildGuideNav($lang, $currentPage, $isAdminGuide); } // Content navigation if ($isAdmin) { return ''; } return $this->buildContentNav($lang, $currentPage); } /** * Build navigation for the guide section. */ private function buildGuideNav(string $lang, string $currentPage, bool $isAdmin): string { $guideRoot = dirname(__DIR__, 2) . '/guide/' . $lang; if (!is_dir($guideRoot)) { return ''; } // Fix: /nl/guide sets page='guide', treat as empty if ($currentPage === 'guide') { $currentPage = ''; } return $this->buildNav($guideRoot, '', $currentPage, $isAdmin, 'guide', $lang); } /** * Build navigation for the content section. */ private function buildContentNav(string $lang, string $currentPage): string { $contentRoot = dirname(__DIR__, 2) . '/content'; if (!is_dir($contentRoot)) { return ''; } // Normalize current page: remove extension $currentPage = preg_replace('/\.(md|php|html)$/', '', $currentPage); return $this->buildNav($contentRoot, '', $currentPage, false, 'content', $lang); } /** * Recursively build navigation from a directory structure. * * @param string $dir Current directory * @param string $relPath Relative path from root * @param string $currentPage Current page path * @param bool $isAdmin Whether this is admin context * @param string $mode 'guide' or 'content' * @param string $lang Current language * @param int $depth Nesting depth (0 = top level) */ private function buildNav(string $dir, string $relPath, string $currentPage, bool $isAdmin, string $mode, string $lang, int $depth = 0): string { $items = []; $entries = scandir($dir); // Sort alphabetically (case-insensitive) natcasesort($entries); foreach ($entries as $entry) { if ($entry === '.' || $entry === '..' || $entry === 'assets' || $entry === 'index.md') { continue; } // Skip hidden files/folders (starting with -) if ($entry[0] === '-' || $entry[0] === '.') { continue; } // Skip non-content files in content mode if ($mode === 'content' && is_file($dir . '/' . $entry)) { $ext = pathinfo($entry, PATHINFO_EXTENSION); if (!in_array($ext, ['md', 'php', 'html'])) { continue; } } $fullPath = $dir . '/' . $entry; $itemRelPath = $relPath ? $relPath . '/' . $entry : $entry; if (is_dir($fullPath)) { $parentMd = $dir . '/' . $entry . '.md'; $title = file_exists($parentMd) ? $this->getTitleFromFile($parentMd) : $this->formatTitle($entry); $pageRef = $relPath ? $relPath . '/' . $entry : $entry; $url = $this->buildUrl($pageRef, $lang, $isAdmin, $mode); $children = $this->buildNav($fullPath, $itemRelPath, $currentPage, $isAdmin, $mode, $lang, $depth + 1); // Only show directories that have children or a parent .md if (empty($children) && !file_exists($parentMd)) { continue; } $items[] = [ 'title' => $title, 'url' => $url, 'active' => $this->isActive($pageRef, $currentPage, $mode), 'children' => $children, 'has_children' => !empty($children), ]; } else { $ext = pathinfo($entry, PATHINFO_EXTENSION); if (!in_array($ext, ['md', 'php', 'html'])) { continue; } $slug = pathinfo($entry, PATHINFO_FILENAME); // Skip language-prefixed files (e.g. nl.pagina.md) $langCodes = $this->getLanguageCodes(); foreach ($langCodes as $lc) { if (strpos($slug, $lc . '.') === 0) { $slug = substr($slug, strlen($lc) + 1); break; } } // Skip if this is a parent .md that has a matching directory if (is_dir($dir . '/' . $slug)) { continue; } $pageRef = $relPath ? $relPath . '/' . $slug : $slug; $url = $this->buildUrl($pageRef, $lang, $isAdmin, $mode); $items[] = [ 'title' => $this->getTitleFromFile($fullPath), 'url' => $url, 'active' => $this->isActive($pageRef, $currentPage, $mode), 'children' => '', 'has_children' => false, ]; } } return $this->renderNav($items, $depth); } /** * Build the URL for a page. */ private function buildUrl(string $pageRef, string $lang, bool $isAdmin, string $mode): string { if ($mode === 'guide') { if ($isAdmin) { return '/admin/guide?page=' . urlencode($pageRef) . '&lang=' . urlencode($lang); } return '/' . urlencode($lang) . '/guide?page=' . urlencode($pageRef); } // Content mode return '/' . urlencode($lang) . '/' . urlencode($pageRef); } /** * Check if a nav item is the current page. */ private function isActive(string $pageRef, string $currentPage, string $mode): bool { if ($mode === 'guide' && $currentPage === 'guide') { $currentPage = ''; } $currentPage = preg_replace('/\.(md|php|html)$/', '', $currentPage); return $pageRef === $currentPage; } /** * Get available language codes. */ private function getLanguageCodes(): array { return ['nl', 'en', 'de', 'fr']; } /** * Format a slug into a readable title. */ private function formatTitle(string $slug): string { $title = str_replace('-', ' ', $slug); $title = ucfirst($title); return $title; } /** * Extract the H1 title from a markdown/PHP/HTML file. * Falls back to formatTitle() if no H1 is found. */ private function getTitleFromFile(string $filePath): string { if (!file_exists($filePath)) { return ''; } $content = file_get_contents($filePath); // Match first H1: # Title if (preg_match('/^#\s+(.+)$/m', $content, $matches)) { return trim($matches[1]); } $slug = pathinfo($filePath, PATHINFO_FILENAME); return $this->formatTitle($slug); } /** * Render the navigation items as HTML. * * @param array $items Navigation items * @param int $depth Nesting depth (0 = top level) */ private function renderNav(array $items, int $depth = 0): string { if (empty($items)) { return ''; } $subClass = $depth > 0 ? ' nav-sub' : ''; $html = '