- Moved auto-linking before markdown link processing to avoid conflicts - Added dash-dash (dashed) underline for auto-links instead of dotted - Improved pattern matching to avoid linking inside existing markdown links - Enhanced detection of existing HTML tags and links - Auto-links now have 2px dashed underline with hover effect
464 lines
17 KiB
PHP
464 lines
17 KiB
PHP
<?php
|
|
|
|
require_once 'config.php';
|
|
|
|
$config = include 'config.php';
|
|
|
|
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();
|
|
}
|
|
|
|
$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 = '<h2>Search Results for: "' . htmlspecialchars($query) . '"</h2>';
|
|
|
|
if (empty($this->searchResults)) {
|
|
$content .= '<p>No results found.</p>';
|
|
} else {
|
|
$content .= '<p>Found ' . count($this->searchResults) . ' results:</p>';
|
|
foreach ($this->searchResults as $result) {
|
|
$content .= '<div class="card mb-3">';
|
|
$content .= '<div class="card-body">';
|
|
$content .= '<h5 class="card-title"><a href="' . htmlspecialchars($result['url']) . '">' . htmlspecialchars($result['title']) . '</a></h5>';
|
|
$content .= '<p class="card-text text-muted">' . htmlspecialchars($result['path']) . '</p>';
|
|
$content .= '<p class="card-text">' . htmlspecialchars($result['snippet']) . '</p>';
|
|
$content .= '</div></div>';
|
|
}
|
|
}
|
|
|
|
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('/### (.+)/', '<h3>$1</h3>', $body);
|
|
$body = preg_replace('/## (.+)/', '<h2>$1</h2>', $body);
|
|
$body = preg_replace('/# (.+)/', '<h1>$1</h1>', $body);
|
|
$body = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $body);
|
|
$body = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $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('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $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/', '</p><p>', $body);
|
|
$body = '<p>' . $body . '</p>';
|
|
$body = preg_replace('/<p><\/p>/', '', $body);
|
|
$body = preg_replace('/<p>(<h[1-6]>)/', '$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('/<a[^>]*>/', $text) ||
|
|
preg_match('/href=/', $text)) {
|
|
return $text; // Don't link existing links
|
|
}
|
|
|
|
return '<a href="?page=' . $pagePath . '" class="auto-link" title="Ga naar ' . htmlspecialchars($pageTitle) . '">' . $text . '</a>';
|
|
};
|
|
|
|
$content = preg_replace_callback($pattern, $replacement, $content);
|
|
}
|
|
|
|
return $content;
|
|
}
|
|
|
|
return '<a href="?page=' . $pagePath . '" class="auto-link" title="Ga naar ' . htmlspecialchars($pageTitle) . '">' . $matches[0] . '</a>';
|
|
};
|
|
|
|
$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>(.*?)<\/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 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();
|
|
|
|
$template = file_get_contents($this->config['templates_dir'] . '/layout.html');
|
|
|
|
$template = str_replace('{{site_title}}', $this->config['site_title'], $template);
|
|
$template = str_replace('{{page_title}}', $page['title'], $template);
|
|
$template = str_replace('{{content}}', $page['content'], $template);
|
|
$template = str_replace('{{search_query}}', isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '', $template);
|
|
$template = str_replace('{{breadcrumb}}', $breadcrumb, $template);
|
|
|
|
// File info for footer
|
|
$fileInfo = '';
|
|
if (isset($page['file_info'])) {
|
|
$fileInfo = '<i class="bi bi-file-text"></i> Created: ' . htmlspecialchars($page['file_info']['created']) .
|
|
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
|
|
}
|
|
$template = str_replace('{{file_info}}', $fileInfo, $template);
|
|
|
|
$menuHtml = $this->renderMenu($menu);
|
|
|
|
$template = str_replace('{{menu}}', $menuHtml, $template);
|
|
|
|
echo $template;
|
|
}
|
|
|
|
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']);
|
|
$html .= '<li class="nav-item">';
|
|
|
|
if ($hasChildren) {
|
|
$folderId = 'folder-' . str_replace('/', '-', $item['path']);
|
|
$html .= '<span class="nav-link folder-toggle" data-bs-toggle="collapse" data-bs-target="#' . $folderId . '" aria-expanded="false">';
|
|
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
|
|
$html .= '</span>';
|
|
$html .= '<ul class="nav flex-column ms-2 collapse" id="' . $folderId . '">';
|
|
$html .= $this->renderMenu($item['children'], $level + 1);
|
|
$html .= '</ul>';
|
|
} else {
|
|
$html .= '<span class="nav-link folder-disabled" disabled>';
|
|
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
|
|
$html .= '</span>';
|
|
}
|
|
|
|
$html .= '</li>';
|
|
} else {
|
|
$active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : '';
|
|
$html .= '<li class="nav-item">';
|
|
$html .= '<a class="nav-link page-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
|
|
$html .= '</li>';
|
|
}
|
|
}
|
|
return $html;
|
|
}
|
|
}
|
|
|
|
$cms = new CodePressCMS($config);
|
|
$cms->render(); |