*/ private array $config; /** * Plugin-API instantie voor communicatie met de CMS-core. * * @since 2.6.5 * @var PluginAPIInterface|null */ private ?PluginAPIInterface $api = null; /** * Initialiseert de plugin met standaardconfiguratie. * * @since 2.6.5 */ public function __construct() { $this->config = [ 'title' => 'Navigatie', 'viewable' => true, 'type' => 'content', ]; } /** * Stelt de Plugin-API instantie in. * * @since 2.6.5 * @param PluginAPIInterface $api De Plugin-API instantie. * @return void */ public function setAPI(PluginAPIInterface $api): void { $this->api = $api; } /** * Geeft de plugin-configuratie terug. * * @since 2.6.5 * @return array Plugin-configuratie. */ public function getConfig(): array { return $this->config; } /** * Stelt de plugin-configuratie in door deze te mergen met standaardwaarden. * * @since 2.6.5 * @param array $config De configuratie die samengevoegd moet worden. * @return void */ public function setConfig(array $config): void { $this->config = array_merge($this->config, $config); } /** * Geeft het pad naar het CSS-bestand van deze plugin (relatief ten opzichte van de webroot). * * @since 2.6.5 * @return string Het CSS-URL-pad. */ public function getCssUrl(): string { return '/plugins/Navigation/assets/css/navigation.css'; } /** * Genereert de zijbalk-navigatie. * * Detecteert of het om een guide-pagina of een content-pagina gaat en * bouwt de navigatie dienovereenkomstig op. * * @since 2.6.5 * @return string De HTML-navigatie, of een lege string in admin-context. */ 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); } /** * Bouwt de navigatie voor de guide-sectie. * * @since 2.6.5 * @param string $lang De huidige taalcode. * @param string $currentPage De huidige pagina-pad. * @param bool $isAdmin Of dit in admin-context is. * @return string De opgebouwde HTML-navigatie, of een lege string. */ private function buildGuideNav(string $lang, string $currentPage, bool $isAdmin): string { $guideRoot = ($this->api ? $this->api->getProjectRoot() : 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); } /** * Bouwt de navigatie voor de content-sectie. * * @since 2.6.5 * @param string $lang De huidige taalcode. * @param string $currentPage De huidige pagina-pad. * @return string De opgebouwde HTML-navigatie, of een lege string. */ private function buildContentNav(string $lang, string $currentPage): string { $contentRoot = $this->api ? $this->api->getContentDir() : 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); } /** * Bouwt recursief navigatie op vanuit een mapstructuur. * * @since 2.6.5 * @param string $dir Huidige map. * @param string $relPath Relatief pad vanaf de root. * @param string $currentPage Huidige pagina-pad. * @param bool $isAdmin Of dit in admin-context is. * @param string $mode Modus: 'guide' of 'content'. * @param string $lang Huidige taalcode. * @param int $depth Nesting-diepte (0 = topniveau). Standaard 0. * @return string De opgebouwde HTML-navigatie. */ 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); } /** * Bouwt de URL voor een pagina-referentie. * * @since 2.6.5 * @param string $pageRef De pagina-referentie. * @param string $lang De huidige taalcode. * @param bool $isAdmin Of dit in admin-context is. * @param string $mode Modus: 'guide' of 'content'. * @return string De opgebouwde URL. */ 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); } /** * Controleert of een navigatie-item de huidige pagina is. * * @since 2.6.5 * @param string $pageRef De pagina-referentie van het item. * @param string $currentPage Het huidige pagina-pad. * @param string $mode Modus: 'guide' of 'content'. * @return bool True als het item actief is, anders false. */ 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; } /** * Geeft de beschikbare taalcodes terug. * * @since 2.6.5 * @return array Lijst met taalcodes. */ private function getLanguageCodes(): array { return ['nl', 'en', 'de', 'fr']; } /** * Formateert een slug naar een leesbare titel. * * @since 2.6.5 * @param string $slug De te formatteren slug. * @return string De geformatteerde titel. */ private function formatTitle(string $slug): string { $title = str_replace('-', ' ', $slug); $title = ucfirst($title); return $title; } /** * Haalt de H1-titel uit een markdown-, PHP- of HTML-bestand. * * Valt terug op formatTitle() wanneer geen H1 gevonden is. * * @since 2.6.5 * @param string $filePath Pad naar het bestand. * @return string De gevonden titel, of een lege string. */ 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); } /** * Rendert de navigatie-items als HTML. * * @since 2.6.5 * @param array $items Navigatie-items. * @param int $depth Nesting-diepte (0 = topniveau). Standaard 0. * @return string De gegenereerde HTML. */ private function renderNav(array $items, int $depth = 0): string { if (empty($items)) { return ''; } $subClass = $depth > 0 ? ' nav-sub' : ''; $html = ''; return $html; } }