Refactor: Replace sidebar with horizontal navigation bar
- Remove sidebar and toggle functionality - Add Bootstrap navbar with dropdown menus - Move navigation to top between header and content - Update menu rendering for Bootstrap dropdowns - Clean up unused files (header.mustache, sidebar.mustache, sidebar.js) - Add guide link with book icon in footer - Simplify layout structure - Remove duplicate code and fix syntax errors - Add .gitignore for node_modules and other temp files
This commit is contained in:
2078
public/assets/css/bootstrap-icons.css
vendored
Normal file
2078
public/assets/css/bootstrap-icons.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
public/assets/css/bootstrap.min.css
vendored
Normal file
6
public/assets/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/assets/css/bootstrap.min.css.map
Normal file
1
public/assets/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
BIN
public/assets/css/fonts/bootstrap-icons.woff
Normal file
BIN
public/assets/css/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
public/assets/css/fonts/bootstrap-icons.woff2
Normal file
BIN
public/assets/css/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
316
public/assets/css/style.css
Normal file
316
public/assets/css/style.css
Normal file
@@ -0,0 +1,316 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background-color: #f8f9fa;
|
||||
border-right: 1px solid #dee2e6;
|
||||
overflow-y: auto; /* Only sidebar scrolls when needed */
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.3s ease;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
.sidebar-toggle: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;
|
||||
display: none; /* Hidden by default */
|
||||
}
|
||||
.sidebar-toggle-outer: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;
|
||||
}
|
||||
|
||||
body.sidebar-collapsed .sidebar-toggle-outer {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
body:not(.sidebar-collapsed) .sidebar-toggle-outer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Override inline styles */
|
||||
.sidebar-toggle-outer[style*="display: none"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.sidebar-collapsed .sidebar-toggle-outer[style*="display: none"] {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
overflow: hidden; /* Main content container doesn't scroll */
|
||||
transition: margin-left 0.3s ease;
|
||||
margin-left: 250px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
flex: 1;
|
||||
overflow-y: auto; /* Only content-inner scrolls when needed */
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.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;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.folder-toggle:hover {
|
||||
background-color: #e9ecef !important;
|
||||
}
|
||||
.folder-toggle[aria-expanded=true] {
|
||||
background-color: #dee2e6 !important;
|
||||
color: #212529 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.folder-toggle .arrow {
|
||||
margin-right: 8px;
|
||||
transition: transform 0.2s;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.folder-toggle[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;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.page-link:hover {
|
||||
color: #212529 !important;
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
.page-link.active {
|
||||
background-color: rgba(13, 110, 253, 0.1) !important;
|
||||
color: #0d6efd !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
flex: 1;
|
||||
min-width: 0; /* Important for text truncation in flexbox */
|
||||
}
|
||||
|
||||
.file-info {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
flex: 1;
|
||||
min-width: 0; /* Important for text truncation in flexbox */
|
||||
}
|
||||
|
||||
.file-info i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: #6c757d;
|
||||
max-width: 250px !important;
|
||||
display: inline-block !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.page-title:hover {
|
||||
color: #495057;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.file-details {
|
||||
color: #6c757d;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.site-info a {
|
||||
color: #0d6efd;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.guide-link {
|
||||
color: white !important;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.site-title-link {
|
||||
color: white !important;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: auto; /* Push footer to bottom */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.site-info a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.auto-link {
|
||||
color: #0d6efd;
|
||||
text-decoration: none;
|
||||
border-bottom: 2px dashed #0d6efd;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.auto-link: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;
|
||||
}
|
||||
.card-title a: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;
|
||||
}
|
||||
.folder-disabled .arrow {
|
||||
margin-right: 8px;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background-color: #f8f9fa;
|
||||
border-right: 1px solid #dee2e6;
|
||||
overflow-y: auto; /* Only sidebar scrolls when needed */
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.3s ease;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
z-index: 999;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
1
public/assets/css/style.css.map
Normal file
1
public/assets/css/style.css.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["style.scss"],"names":[],"mappings":"AAAA;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAOJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;;AAKJ;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;;;AAOJ;EACI;;;AAKZ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAGJ;EACI;;;AAQI;EACI;;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AAMhB;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;;AAGJ;EACI;EACA;EACA;;;AAQI;EACI;;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;AADJ;EACI;;;AAMhB;EACI;EACA;;;AAIA;EACI;EACA;;AAEA;EACI;;;AAKZ;EACI;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;;AAIR;EACI;;;AAIA;EACI;EACA;;AAEA;EACI;;;AAKZ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;EACA;EACA;;;AAIR;EACI;IACI","file":"style.css"}
|
||||
11
public/assets/favicon.svg
Normal file
11
public/assets/favicon.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- < -->
|
||||
<path d="M8 8 L3 16 L8 24" stroke="#ffffff" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
<!-- / -->
|
||||
<path d="M12 24 L18 8" stroke="#ffffff" stroke-width="3" stroke-linecap="round"/>
|
||||
|
||||
<!-- .. -->
|
||||
<circle cx="22" cy="20" r="2" fill="#ffffff"/>
|
||||
<circle cx="28" cy="20" r="2" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 442 B |
11
public/assets/icon.svg
Normal file
11
public/assets/icon.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- < -->
|
||||
<path d="M8 8 L3 16 L8 24" stroke="#ffffff" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
<!-- / -->
|
||||
<path d="M12 24 L18 8" stroke="#ffffff" stroke-width="3" stroke-linecap="round"/>
|
||||
|
||||
<!-- .. -->
|
||||
<circle cx="22" cy="20" r="2" fill="#ffffff"/>
|
||||
<circle cx="28" cy="20" r="2" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 442 B |
7
public/assets/js/app.js
Normal file
7
public/assets/js/app.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// Main application JavaScript
|
||||
// This file contains general application functionality
|
||||
|
||||
// Initialize application when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('CodePress CMS initialized');
|
||||
});
|
||||
7
public/assets/js/bootstrap.bundle.min.js
vendored
Normal file
7
public/assets/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/assets/js/bootstrap.bundle.min.js.map
Normal file
1
public/assets/js/bootstrap.bundle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
../engine
|
||||
436
public/index.php
436
public/index.php
@@ -1,442 +1,10 @@
|
||||
<?php
|
||||
|
||||
|
||||
require_once __DIR__ . '/../engine/core/config.php';
|
||||
require_once __DIR__ . '/../engine/core/index.php';
|
||||
|
||||
$config = include __DIR__ . '/../engine/core/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 (is_dir($filePath)) {
|
||||
// Check for index files in directory
|
||||
if (file_exists($filePath . '/index.md')) {
|
||||
$actualFilePath = $filePath . '/index.md';
|
||||
$result = $this->parseMarkdown(file_get_contents($actualFilePath));
|
||||
} elseif (file_exists($filePath . '/index.php')) {
|
||||
$actualFilePath = $filePath . '/index.php';
|
||||
$result = $this->parsePHP($actualFilePath);
|
||||
} elseif (file_exists($filePath . '/index.html')) {
|
||||
$actualFilePath = $filePath . '/index.html';
|
||||
$result = $this->parseHTML(file_get_contents($actualFilePath));
|
||||
} else {
|
||||
// Generate directory listing
|
||||
return $this->generateDirectoryListing($filePath, $page);
|
||||
}
|
||||
} 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 generateDirectoryListing($dirPath, $urlPath) {
|
||||
$title = ucfirst(basename($dirPath));
|
||||
$content = '<div class="row">';
|
||||
|
||||
$items = scandir($dirPath);
|
||||
sort($items);
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item[0] === '.') continue;
|
||||
|
||||
$path = $dirPath . '/' . $item;
|
||||
$relativePath = $urlPath . '/' . $item;
|
||||
$itemName = ucfirst(pathinfo($item, PATHINFO_FILENAME));
|
||||
|
||||
if (is_dir($path)) {
|
||||
$content .= '<div class="col-md-6 mb-4">';
|
||||
$content .= '<div class="card h-100 border-0 rounded-0 bg-light">';
|
||||
$content .= '<div class="card-body">';
|
||||
$content .= '<h3 class="h5 card-title"><a href="?page=' . $relativePath . '" class="text-decoration-none text-dark"><i class="bi bi-folder me-2"></i> ' . $itemName . '</a></h3>';
|
||||
$content .= '</div></div></div>';
|
||||
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
|
||||
// Remove extension from URL for cleaner links
|
||||
$cleanPath = preg_replace('/\.[^.]+$/', '', $relativePath);
|
||||
|
||||
// Get preview content
|
||||
$preview = '';
|
||||
$fileContent = file_get_contents($path);
|
||||
|
||||
// Extract title if possible
|
||||
$fileTitle = $itemName;
|
||||
if (preg_match('/^#\s+(.+)$/m', $fileContent, $matches)) {
|
||||
$fileTitle = trim($matches[1]);
|
||||
}
|
||||
|
||||
// Extract preview text (first paragraph)
|
||||
$fileContent = strip_tags($this->parseMarkdown($fileContent)['content']);
|
||||
$preview = substr($fileContent, 0, 150) . '...';
|
||||
|
||||
$content .= '<div class="col-md-6 mb-4">';
|
||||
$content .= '<div class="card h-100 border rounded-0">';
|
||||
$content .= '<div class="card-body">';
|
||||
$content .= '<h3 class="h5 card-title"><a href="?page=' . $cleanPath . '" class="text-decoration-none text-primary">' . $fileTitle . '</a></h3>';
|
||||
$content .= '<p class="card-text text-muted small">' . $preview . '</p>';
|
||||
$content .= '<a href="?page=' . $cleanPath . '" class="btn btn-sm btn-outline-primary rounded-0">Lees meer</a>';
|
||||
$content .= '</div></div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
$content .= '</div>';
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'content' => $content
|
||||
];
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 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 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 {
|
||||
// Check if directory has index file
|
||||
$dirPath = $this->config['content_dir'] . '/' . $path;
|
||||
$hasIndex = file_exists($dirPath . '/index.md') || file_exists($dirPath . '/index.php') || file_exists($dirPath . '/index.html');
|
||||
|
||||
// Always make breadcrumb items clickable, CMS will generate index if missing
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
// Block direct access to content files
|
||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
if (strpos($requestUri, '/content/') !== false) {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
// Router file for PHP development server to handle security and static files
|
||||
|
||||
$requestUri = $_SERVER['REQUEST_URI'];
|
||||
$parsedUrl = parse_url($requestUri);
|
||||
$path = $parsedUrl['path'];
|
||||
|
||||
// Block direct access to content directory
|
||||
if (strpos($path, '/content/') === 0) {
|
||||
http_response_code(403);
|
||||
echo '<h1>403 - Forbidden</h1><p>Direct access to content files is not allowed.</p>';
|
||||
return true;
|
||||
}
|
||||
|
||||
// Block access to sensitive files
|
||||
$sensitiveFiles = ['.htaccess', 'config.php'];
|
||||
foreach ($sensitiveFiles as $file) {
|
||||
if (basename($path) === $file && dirname($path) === '/') {
|
||||
http_response_code(403);
|
||||
echo '<h1>403 - Forbidden</h1><p>Access to this file is not allowed.</p>';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Serve static files from engine/assets
|
||||
if (strpos($path, '/engine/') === 0) {
|
||||
$filePath = __DIR__ . $path;
|
||||
if (file_exists($filePath)) {
|
||||
// Set appropriate content type
|
||||
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||||
$mimeTypes = [
|
||||
'css' => 'text/css',
|
||||
'js' => 'application/javascript',
|
||||
'svg' => 'image/svg+xml',
|
||||
'woff' => 'font/woff',
|
||||
'woff2' => 'font/woff2',
|
||||
'ttf' => 'font/ttf'
|
||||
];
|
||||
|
||||
if (isset($mimeTypes[$extension])) {
|
||||
header('Content-Type: ' . $mimeTypes[$extension]);
|
||||
}
|
||||
|
||||
// Serve the file
|
||||
readfile($filePath);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Route all other requests to index.php
|
||||
include __DIR__ . '/index.php';
|
||||
return true;
|
||||
Reference in New Issue
Block a user