partial}}) $template = preg_replace_callback('/{{>([^}]+)}}/', function($matches) use ($data) { $partialName = $matches[1]; return $data[$partialName] ?? $matches[0]; }, $template); // Handle conditional blocks foreach ($data as $key => $value) { if (is_array($value) || (is_string($value) && !empty($value))) { // Handle {{#key}}...{{/key}} blocks $pattern = '/{{#' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s'; if (preg_match($pattern, $template, $matches)) { $replacement = $matches[1]; $template = preg_replace($pattern, $replacement, $template); } // Handle {{^key}}...{{/key}} blocks (negative condition) $pattern = '/{{\^' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s'; $template = preg_replace($pattern, '', $template); } else { // Handle empty blocks $pattern = '/{{#' . preg_quote($key, '/') . '}}.*?{{\/' . preg_quote($key, '/') . '}}/s'; $template = preg_replace($pattern, '', $template); // Handle {{^key}}...{{/key}} blocks (show when empty) $pattern = '/{{\^' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s'; if (preg_match_all($pattern, $template, $matches)) { foreach ($matches[1] as $match) { $template = preg_replace('/{{\^' . preg_quote($key, '/') . '}}.*?{{\/' . preg_quote($key, '/') . '}}/s', $match, $template, 1); } } } } // Handle variable replacements foreach ($data as $key => $value) { // Handle triple braces for unescaped HTML content if (strpos($template, '{{{' . $key . '}}}') !== false) { $template = str_replace('{{{' . $key . '}}}', $value, $template); } // Handle double braces for escaped content elseif (strpos($template, '{{' . $key . '}}') !== false) { $template = str_replace('{{' . $key . '}}', htmlspecialchars($value, ENT_QUOTES, 'UTF-8'), $template); } } return $template; } } $config = include 'config.php'; /** * CodePressCMS - Lightweight file-based content management system * * Features: * - Markdown, PHP, and HTML content support * - Dynamic navigation with dropdown menus * - Search functionality * - Breadcrumb navigation * - Auto-linking between pages * - Configurable via JSON * - Responsive design with Bootstrap */ /** * CodePressCMS - Lightweight file-based content management system * * Features: * - Markdown, PHP, and HTML content support * - Dynamic navigation with dropdown menus * - Search functionality * - Breadcrumb navigation * - Auto-linking between pages * - Configurable via JSON * - Responsive design with Bootstrap */ class CodePressCMS { private $config; private $menu = []; private $searchResults = []; public function __construct($config) { $this->config = $config; $this->buildMenu(); if (isset($_GET['search'])) { $this->performSearch($_GET['search']); } } private function buildMenu() { $this->menu = $this->scanDirectory($this->config['content_dir'], ''); } private function scanDirectory($dir, $prefix) { if (!is_dir($dir)) return []; $items = scandir($dir); sort($items); $result = []; foreach ($items as $item) { if ($item[0] === '.') continue; $path = $dir . '/' . $item; $relativePath = $prefix ? $prefix . '/' . $item : $item; if (is_dir($path)) { $result[] = [ 'type' => 'folder', 'title' => ucfirst($item), 'path' => $relativePath, 'children' => $this->scanDirectory($path, $relativePath) ]; } elseif (preg_match('/\.(md|php|html)$/', $item)) { $title = ucfirst(pathinfo($item, PATHINFO_FILENAME)); $result[] = [ 'type' => 'file', 'title' => $title, 'path' => $relativePath, 'url' => '?page=' . $relativePath ]; } } return $result; } private function performSearch($query) { $this->searchResults = []; $this->searchInDirectory($this->config['content_dir'], '', $query); } 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) ]; } } } } 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 . '...'; } 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']; $page = preg_replace('/\.[^.]+$/', '', $page); $filePath = $this->config['content_dir'] . '/' . $page; $actualFilePath = null; if (file_exists($filePath . '.md')) { $actualFilePath = $filePath . '.md'; $result = $this->parseMarkdown(file_get_contents($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)); } elseif ($extension === 'php') { $result = $this->parsePHP($actualFilePath); } elseif ($extension === 'html') { $result = $this->parseHTML(file_get_contents($actualFilePath)); } } if (isset($result) && $actualFilePath) { $result['file_info'] = $this->getFileInfo($actualFilePath); return $result; } return $this->getError404(); } 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']) ]; } 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]; } private function getSearchResults() { $query = $_GET['search']; $content = '

Search Results for: "' . htmlspecialchars($query) . '"

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

No results found.

'; } else { $content .= '

Found ' . count($this->searchResults) . ' results:

'; 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 ]; } private function parseMarkdown($content) { $lines = explode("\n", $content); $title = ''; $body = ''; $inBody = false; foreach ($lines as $line) { if (!$inBody && preg_match('/^#\s+(.+)$/', $line, $matches)) { $title = $matches[1]; $inBody = true; } elseif ($inBody || trim($line) !== '') { $body .= $line . "\n"; $inBody = true; } } $body = preg_replace('/### (.+)/', '

$1

', $body); $body = preg_replace('/## (.+)/', '

$1

', $body); $body = preg_replace('/# (.+)/', '

$1

', $body); $body = preg_replace('/\*\*(.+?)\*\*/', '$1', $body); $body = preg_replace('/\*(.+?)\*/', '$1', $body); // Auto-link page titles to existing content pages (before markdown link processing) $body = $this->autoLinkPageTitles($body); // Convert Markdown links to HTML links $body = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '$1', $body); // 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); $body = preg_replace('/\n\n/', '

', $body); $body = '

' . $body . '

'; $body = preg_replace('/

<\/p>/', '', $body); $body = preg_replace('/

()/', '$1', $body); $body = preg_replace('/(<\/h[1-6]>)<\/p>/', '$1', $body); return [ 'title' => $title ?: 'Untitled', 'content' => $body ]; } private function autoLinkPageTitles($content) { // 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 to avoid partial matches $pattern = '/\b' . preg_quote($pageTitle, '/') . '\b/i'; // Replace with link, but avoid linking inside existing links, headings, or markdown $replacement = function($matches) use ($pageTitle, $pagePath) { $text = $matches[0]; // Check if we're inside an existing link or markdown syntax if (preg_match('/\[.*?\]\(.*?\)/', $text) || preg_match('/\[.*?\]:/', $text) || preg_match('/]*>/', $text) || preg_match('/href=/', $text)) { return $text; // Don't link existing links } return '' . $text . ''; }; $content = preg_replace_callback($pattern, $replacement, $content); } return $content; } private function getAllPageTitles() { $pages = []; $this->scanForPageTitles($this->config['content_dir'], '', $pages); return $pages; } 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; } } } } 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; } private function parsePHP($filePath) { ob_start(); $title = 'Untitled'; include $filePath; $content = ob_get_clean(); return [ 'title' => $title, 'content' => $content ]; } private function parseHTML($content) { $title = 'Untitled'; if (preg_match('/<title>(.*?)<\/title>/i', $content, $matches)) { $title = strip_tags($matches[1]); } elseif (preg_match('/<h1[^>]*>(.*?)<\/h1>/i', $content, $matches)) { $title = strip_tags($matches[1]); } return [ 'title' => $title, 'content' => $content ]; } private function isContentDirEmpty() { $contentDir = $this->config['content_dir']; if (!is_dir($contentDir)) { return true; } $files = scandir($contentDir); $files = array_diff($files, ['.', '..']); return empty($files); } private function getGuidePage() { $lang = $this->detectLanguage(); $guideFile = __DIR__ . '/../../guide/' . $lang . '.md'; if (!file_exists($guideFile)) { $guideFile = __DIR__ . '/../../guide/en.md'; // Fallback to English } $content = file_get_contents($guideFile); $result = $this->parseMarkdown($content); // Set special title for guide $result['title'] = 'Handleiding - CodePress CMS'; return $result; } 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 } private function getError404() { return [ 'title' => 'Page Not Found', 'content' => '<h1>404 - Page Not Found</h1><p>The page you are looking for does not exist.</p>' ]; } public function getMenu() { return $this->menu; } public function render() { $page = $this->getPage(); $menu = $this->getMenu(); $breadcrumb = $this->getBreadcrumb(); // 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['homepage'], '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' ]; // File info for footer if (isset($page['file_info'])) { $templateData['file_info'] = 'Created: ' . htmlspecialchars($page['file_info']['created']) . ' | 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'] = ''; } // File info for footer if (isset($page['file_info'])) { $templateData['file_info'] = 'Created: ' . htmlspecialchars($page['file_info']['created']) . ' | Modified: ' . htmlspecialchars($page['file_info']['modified']); } else { $templateData['file_info'] = ''; } // 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 partials manually $hasContent = !$this->isContentDirEmpty() && !isset($_GET['guide']); $headerContent = file_get_contents($this->config['templates_dir'] . '/assets/header.mustache'); if (!$hasContent) { // Remove the link from header when no content $headerContent = preg_replace('/<a href="[^"]*" class="site-title-link">\s*<h1[^>]*>(.*?)<\/h1>\s*<\/a>/', '<h1 class="h3 mb-0">$1</h1>', $headerContent); } $footerContent = file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache'); if (!$hasContent) { // Remove guide link from footer when no content $footerContent = preg_replace('/<span class="file-details">\s*\|\s*<a href="\?guide"[^>]*>Handleiding<\/a><\/span>/', '', $footerContent); } // 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>'; // Determine content type and load appropriate template $pagePath = $_GET['page'] ?? $this->config['default_page']; $pagePath = preg_replace('/\.[^.]+$/', '', $pagePath); $filePath = $this->config['content_dir'] . '/' . $pagePath; $contentType = 'markdown'; // default if (file_exists($filePath . '.md')) { $contentType = 'markdown'; } elseif (file_exists($filePath . '.php')) { $contentType = 'php'; } elseif (file_exists($filePath . '.html')) { $contentType = 'html'; } $contentTemplateFile = $this->config['templates_dir'] . '/' . $contentType . '_content.mustache'; $contentTemplate = file_exists($contentTemplateFile) ? file_get_contents($contentTemplateFile) : '<div class="content">{{{content}}}</div>'; $partials = [ 'navigation' => file_get_contents($this->config['templates_dir'] . '/assets/navigation.mustache'), 'footer' => file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache'), 'content_template' => $contentTemplate ]; // Replace partials in template $template = file_get_contents($this->config['templates_dir'] . '/layout.mustache'); $template = str_replace('{{>navigation}}', $partials['navigation'], $template); $template = str_replace('{{>footer}}', $partials['footer'], $template); // Render template with data $renderedTemplate = SimpleTemplate::render($template, $templateData); echo $renderedTemplate; } private function getBreadcrumb() { if (isset($_GET['search'])) { return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li><li class="breadcrumb-item active">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">Home</li></ol></nav>'; } $parts = explode('/', $page); $breadcrumb = '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li>'; $path = ''; foreach ($parts as $i => $part) { $path .= ($path ? '/' : '') . $part; $title = ucfirst($part); if ($i === count($parts) - 1) { $breadcrumb .= '<li class="breadcrumb-item active">' . $title . '</li>'; } else { $breadcrumb .= '<li class="breadcrumb-item"><a href="?page=' . $path . '">' . $title . '</a></li>'; } } $breadcrumb .= '</ol></nav>'; return $breadcrumb; } 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']); $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']); $html .= '</a>'; $html .= '<ul class="dropdown-menu" aria-labelledby="' . $folderId . '">'; $html .= $this->renderMenu($item['children'], $level + 1); $html .= '</ul>'; $html .= '</li>'; } else { $html .= '<li class="nav-item">'; $html .= '<span class="nav-link text-muted">' . htmlspecialchars($item['title']) . '</span>'; $html .= '</li>'; } } else { // Only show files that are directly in root (not in folders) 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['homepage']) { 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>'; } } } return $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'; } 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; } }