diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e96e09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Dependencies +node_modules/ +package-lock.json + +# Build outputs +*.log +.DS_Store +Thumbs.db + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# Cache +.cache/ +.sass-cache/ + +# Temporary files +*.tmp +*.temp \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..90848ce --- /dev/null +++ b/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "mustache/mustache": "^3.0" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..582bf01 --- /dev/null +++ b/composer.lock @@ -0,0 +1,72 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "44b7b2c58a30151ae57314c84e2abdd5", + "packages": [ + { + "name": "mustache/mustache", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/mustache.php.git", + "reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/176b6b21d68516dd5107a63ab71b0050e518b7a4", + "reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.19.3", + "yoast/phpunit-polyfills": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mustache\\": "src/" + }, + "classmap": [ + "src/compat.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" + } + ], + "description": "A Mustache implementation in PHP.", + "homepage": "https://github.com/bobthecow/mustache.php", + "keywords": [ + "mustache", + "templating" + ], + "support": { + "issues": "https://github.com/bobthecow/mustache.php/issues", + "source": "https://github.com/bobthecow/mustache.php/tree/v3.0.0" + }, + "time": "2025-06-28T18:28:20+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/composer.phar b/composer.phar new file mode 100755 index 0000000..02740c5 Binary files /dev/null and b/composer.phar differ diff --git a/config.php b/config.php deleted file mode 100644 index efde3e6..0000000 --- a/config.php +++ /dev/null @@ -1,16 +0,0 @@ - 'CodePress', - 'site_description' => 'A simple PHP CMS', - 'base_url' => '/', - 'content_dir' => __DIR__ . '/public/content', - 'templates_dir' => __DIR__ . '/templates', - 'cache_dir' => __DIR__ . '/cache', - 'default_page' => 'home', - 'error_404' => '404', - 'markdown_enabled' => true, - 'php_enabled' => true, - 'bootstrap_version' => '5.3.0', - 'jquery_version' => '3.7.1' -]; \ No newline at end of file diff --git a/engine/assets/css/style.scss b/engine/assets/css/style.scss new file mode 100644 index 0000000..b1cb023 --- /dev/null +++ b/engine/assets/css/style.scss @@ -0,0 +1,246 @@ +// SCSS variables +$primary: #0d6efd; +$secondary: #6c757d; +$success: #198754; +$info: #0dcaf0; +$warning: #ffc107; +$danger: #dc3545; +$light: #f8f9fa; +$dark: #212529; + +// SCSS mixins +@mixin transition($properties...) { + transition: $properties; +} + +// Custom styles +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + height: 100%; +} + +body { + display: flex; + flex-direction: column; + min-height: 100vh; + position: relative; +} + +.main-wrapper { + flex: 1; + display: flex; + flex-direction: column; + min-height: calc(100vh - 70px); +} + +.content-wrapper { + flex: 1; + display: flex; + overflow: hidden; +} + +.sidebar { + width: 250px; + background-color: #f8f9fa; + border-right: 1px solid #dee2e6; + overflow-y: auto; + flex-shrink: 0; + @include transition(transform 0.3s ease); + position: fixed; + top: 70px; + left: 0; + height: calc(100vh - 140px); + z-index: 999; + transform: translateX(0); +} + +.sidebar.collapsed { + transform: translateX(-250px); +} + +.sidebar-toggle { + position: absolute; + top: 15px; + right: 15px; + z-index: 1001; + background: none; + border: none; + cursor: pointer; + @include transition(all 0.3s ease); + font-size: 20px; + color: #6c757d; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + + &:hover { + color: #0d6efd; + background-color: #f8f9fa; + } +} + +.sidebar-toggle-outer { + position: relative; + z-index: 1001; + background-color: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + margin-right: 10px; + color: white !important; + + &:hover { + background-color: rgba(255, 255, 255, 0.3); + color: white !important; + } +} + +.sidebar.collapsed .sidebar-toggle-inner { + right: auto; + left: 15px; +} + +.sidebar.collapsed ~ .main-content { + margin-left: 0; +} + +.sidebar.collapsed .sidebar-toggle-outer { + display: block !important; +} + +.sidebar:not(.collapsed) .sidebar-toggle-outer { + display: none !important; +} + +.main-content { + flex: 1; + overflow-y: auto; + padding: 20px; + @include transition(margin-left 0.3s ease); + margin-left: 250px; +} + +.folder-toggle { + font-weight: bold; + color: #212529 !important; + cursor: pointer; + display: flex; + align-items: center; + padding: 0.5rem 0.75rem; + background-color: #f8f9fa !important; + margin: 0.125rem 0; + @include transition(background-color 0.2s ease); + + &:hover { + background-color: #e9ecef !important; + } + + &[aria-expanded=true] { + background-color: #dee2e6 !important; + color: #212529 !important; + font-weight: 600; + } + + .arrow { + margin-right: 8px; + @include transition(transform 0.2s); + font-size: 0.8em; + } + + &[aria-expanded=true] .arrow { + transform: rotate(90deg); + } +} + +.page-link { + color: #495057 !important; + font-weight: 500; + padding: 0.5rem 0.75rem; + padding-left: 2rem; + background-color: #ffffff !important; + margin: 0.125rem 0; + @include transition(background-color 0.2s ease); + + &:hover { + color: #212529 !important; + background-color: #f8f9fa !important; + } + + &.active { + background-color: #0d6efd !important; + color: #212529 !important; + font-weight: 600; + } +} + +.file-info { + font-size: 0.9rem; + color: #6c757d; +} + +.site-info a { + color: #0d6efd; + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} + +.auto-link { + color: #0d6efd; + text-decoration: none; + border-bottom: 2px dashed #0d6efd; + font-weight: 500; + @include transition(all 0.2s ease); + + &:hover { + color: #0a58ca; + text-decoration: none; + border-bottom-style: solid; + border-bottom-color: #0a58ca; + } +} + +.search-form { + max-width: 300px; +} + +.card-title a { + text-decoration: none; + color: inherit; + + &:hover { + text-decoration: underline; + } +} + +.folder-disabled { + font-weight: 500; + color: #6c757d !important; + cursor: not-allowed; + display: flex; + align-items: center; + padding: 0.5rem 0.75rem; + background-color: #f8f9fa !important; + margin: 0.125rem 0; + opacity: 0.7; + + .arrow { + margin-right: 8px; + font-size: 0.8em; + opacity: 0.6; + } +} + +@media (max-width: 768px) { + .sidebar { + width: 200px; + } +} \ No newline at end of file diff --git a/engine/core/config.php b/engine/core/config.php index 76139ed..e60653b 100644 --- a/engine/core/config.php +++ b/engine/core/config.php @@ -1,16 +1,8 @@ 'CodePress', - 'site_description' => 'A simple PHP CMS', - 'base_url' => '/', 'content_dir' => __DIR__ . '/../../content', 'templates_dir' => __DIR__ . '/../templates', - 'cache_dir' => __DIR__ . '/../../cache', - 'default_page' => 'home', - 'error_404' => '404', - 'markdown_enabled' => true, - 'php_enabled' => true, - 'bootstrap_version' => '5.3.0', - 'jquery_version' => '3.7.1' + 'default_page' => 'home' ]; \ No newline at end of file diff --git a/engine/core/index.php b/engine/core/index.php index 4f6a101..4e901fa 100644 --- a/engine/core/index.php +++ b/engine/core/index.php @@ -2,6 +2,52 @@ require_once 'config.php'; +// Simple template rendering without Mustache for now +class SimpleTemplate { + public static function render($template, $data) { + // Handle conditional blocks first + 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'; class CodePressCMS { @@ -104,6 +150,16 @@ class CodePressCMS { 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); @@ -348,6 +404,46 @@ private function autoLinkPageTitles($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', @@ -364,27 +460,59 @@ private function autoLinkPageTitles($content) { $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); + // 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'] + ]; // File info for footer - $fileInfo = ''; if (isset($page['file_info'])) { - $fileInfo = ' Created: ' . htmlspecialchars($page['file_info']['created']) . - ' | Modified: ' . htmlspecialchars($page['file_info']['modified']); + $templateData['file_info'] = 'Created: ' . htmlspecialchars($page['file_info']['created']) . + ' | Modified: ' . htmlspecialchars($page['file_info']['modified']); + } else { + $templateData['file_info'] = ''; } - $template = str_replace('{{file_info}}', $fileInfo, $template); - $menuHtml = $this->renderMenu($menu); + // Check if content exists for guide link + $hasContent = !$this->isContentDirEmpty(); + $templateData['has_content'] = $hasContent; - $template = str_replace('{{menu}}', $menuHtml, $template); + // Don't show site title link on guide page + $templateData['show_site_link'] = !$this->isContentDirEmpty() && !isset($_GET['guide']); - echo $template; + // 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('/\s*]*>(.*?)<\/h1>\s*<\/a>/', '

$1

', $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('/\s*\|\s*
]*>Handleiding<\/a><\/span>/', '', $footerContent); + } + + $partials = [ + 'navigation' => file_get_contents($this->config['templates_dir'] . '/assets/navigation.mustache'), + 'footer' => file_get_contents($this->config['templates_dir'] . '/assets/footer.mustache') + ]; + + // 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 + echo SimpleTemplate::render($template, $templateData); } private function getBreadcrumb() { @@ -423,33 +551,27 @@ private function autoLinkPageTitles($content) { foreach ($items as $item) { if ($item['type'] === 'folder') { $hasChildren = !empty($item['children']); - $html .= '