Fix guide template variable replacement and enhance documentation

- Fix template variable replacement in guide pages by removing {{}} brackets
- Escape code blocks in guide markdown to prevent template processing
- Completely rewrite guide documentation with comprehensive CMS features
- Add bilingual guide support (English/Dutch) with detailed examples
- Enhance CodePressCMS core with improved guide page handling
- Update template system with better layout and footer components
- Improve language files with additional translations
- Update configuration with enhanced theme and language settings

Resolves issue where guide pages were showing replaced template variables
instead of displaying them as documentation examples.
This commit is contained in:
2025-11-26 16:50:49 +01:00
parent f5ac28a74e
commit 9c5a43c5ce
17 changed files with 1684 additions and 462 deletions

View File

@@ -21,11 +21,12 @@
* @license MIT
*/
class CodePressCMS {
private $config;
public $config;
public $currentLanguage;
public $searchResults = [];
private $menu = [];
private $searchResults = [];
private $currentLanguage;
private $translations = [];
private $pluginManager;
/**
* Constructor - Initialize the CMS with configuration
@@ -43,6 +44,14 @@ class CodePressCMS {
$this->currentLanguage = $this->getCurrentLanguage();
$this->translations = $this->loadTranslations($this->currentLanguage);
// Initialize plugin manager
require_once __DIR__ . '/../plugin/PluginManager.php';
require_once __DIR__ . '/../plugin/CMSAPI.php';
$this->pluginManager = new PluginManager(__DIR__ . '/../../../plugins');
$api = new CMSAPI($this);
$this->pluginManager->setAPI($api);
$this->buildMenu();
if (isset($_GET['search'])) {
@@ -67,7 +76,7 @@ class CodePressCMS {
*
* @return array Available languages with their codes and names
*/
private function getAvailableLanguages() {
public function getAvailableLanguages() {
$langDir = __DIR__ . '/../../lang/';
$languages = [];
@@ -444,16 +453,59 @@ class CodePressCMS {
];
}
/**
* Parse metadata from content
*
* @param string $content Raw content
* @return array Parsed metadata and content without meta block
*/
private function parseMetadata($content) {
$metadata = [];
$contentWithoutMeta = $content;
// Check for YAML frontmatter (--- at start and end)
if (preg_match('/^---\s*\n(.*?)\n---\s*\n(.*)$/s', $content, $matches)) {
$metaContent = $matches[1];
$contentWithoutMeta = $matches[2];
// Parse YAML-like metadata
$lines = explode("\n", $metaContent);
foreach ($lines as $line) {
if (strpos($line, ':') !== false) {
list($key, $value) = explode(':', $line, 2);
$key = trim($key);
$value = trim($value, ' "\'');
// Handle boolean values
if ($value === 'true') $value = true;
elseif ($value === 'false') $value = false;
$metadata[$key] = $value;
}
}
}
return [
'metadata' => $metadata,
'content' => $contentWithoutMeta
];
}
/**
* Parse Markdown content to HTML using League CommonMark
*
* @param string $content Raw Markdown content
* @return array Parsed content with title and body
*/
private function parseMarkdown($content, $actualFilePath = '') {
// Extract title from first H1
$title = '';
if (preg_match('/^#\s+(.+)$/m', $content, $matches)) {
public function parseMarkdown($content, $actualFilePath = '') {
// Parse metadata first
$parsed = $this->parseMetadata($content);
$metadata = $parsed['metadata'];
$content = $parsed['content'];
// Extract title from first H1 or metadata
$title = $metadata['title'] ?? '';
if (empty($title) && preg_match('/^#\s+(.+)$/m', $content, $matches)) {
$title = trim($matches[1]);
}
@@ -495,8 +547,10 @@ class CodePressCMS {
$body = preg_replace('/href="\/([^"]+)"/', 'href="?page=$1"', $body);
return [
'title' => $cleanName ?: 'Untitled',
'content' => $body
'title' => $title ?: $cleanName ?: 'Untitled',
'content' => $body,
'metadata' => $metadata,
'layout' => $metadata['layout'] ?? 'sidebar-content'
];
}
@@ -544,7 +598,7 @@ class CodePressCMS {
*
* @return array Associative array of page paths to titles
*/
private function getAllPageTitles() {
public function getAllPageTitles() {
$pages = [];
$this->scanForPageTitles($this->config['content_dir'], '', $pages);
return $pages;
@@ -708,19 +762,36 @@ class CodePressCMS {
* @return array Parsed content with title and body
*/
private function parsePHP($filePath) {
// Read file content first to extract metadata
$fileContent = file_get_contents($filePath);
$parsed = $this->parseMetadata($fileContent);
$metadata = $parsed['metadata'];
// Extract title from metadata or PHP variables
$title = $metadata['title'] ?? '';
ob_start();
// Make metadata available to the included file
$pageMetadata = $metadata;
include $filePath;
$content = ob_get_clean();
// Remove any remaining metadata from PHP output
$content = preg_replace('/^---\s*\n.*?\n---\s*\n/s', '', $content);
// Remove metadata from content if it was included
$parsed = $this->parseMetadata($content);
$content = $parsed['content'];
// Extract filename for title
$filename = basename($filePath);
$cleanName = $this->formatDisplayName($filename);
return [
'title' => $cleanName ?: 'Untitled',
'content' => $content
'title' => $title ?: $cleanName ?: 'Untitled',
'content' => $content,
'metadata' => $metadata,
'layout' => $metadata['layout'] ?? 'sidebar-content'
];
}
@@ -731,13 +802,30 @@ class CodePressCMS {
* @return array Parsed content with title and body
*/
private function parseHTML($content, $actualFilePath = '') {
// Parse metadata first
$parsed = $this->parseMetadata($content);
$metadata = $parsed['metadata'];
$content = $parsed['content'];
// Extract title from metadata or HTML tags
$title = $metadata['title'] ?? '';
if (empty($title)) {
if (preg_match('/<title>(.*?)<\/title>/i', $content, $matches)) {
$title = trim(strip_tags($matches[1]));
} elseif (preg_match('/<h1[^>]*>(.*?)<\/h1>/i', $content, $matches)) {
$title = trim(strip_tags($matches[1]));
}
}
// Extract filename for title
$filename = basename($actualFilePath);
$cleanName = $this->formatDisplayName($filename);
return [
'title' => $cleanName ?: 'Untitled',
'content' => $content
'title' => $title ?: $cleanName ?: 'Untitled',
'content' => $content,
'metadata' => $metadata,
'layout' => $metadata['layout'] ?? 'sidebar-content'
];
}
@@ -746,7 +834,7 @@ class CodePressCMS {
*
* @return bool True if content directory is empty or doesn't exist
*/
private function isContentDirEmpty() {
public function isContentDirEmpty() {
$contentDir = $this->config['content_dir'];
if (!is_dir($contentDir)) {
return true;
@@ -763,21 +851,60 @@ class CodePressCMS {
*
* @return array Guide page data
*/
private function getGuidePage() {
private function getGuidePage() {
$lang = $this->currentLanguage;
$guideFile = __DIR__ . '/../../../guide/' . $lang . '.codepress.md';
if (!file_exists($guideFile)) {
$guideFile = __DIR__ . '/../../../guide/en.codepress.md'; // Fallback to English
}
}
$content = file_get_contents($guideFile);
$result = $this->parseMarkdown($content);
// Parse metadata first
$parsed = $this->parseMetadata($content);
$metadata = $parsed['metadata'];
$contentWithoutMeta = $parsed['content'];
// Include autoloader
require_once __DIR__ . '/../../../vendor/autoload.php';
// Configure CommonMark environment
$config = [
'html_input' => 'strip',
'allow_unsafe_links' => false,
'max_nesting_level' => 100,
];
// Create environment with extensions
$environment = new \League\CommonMark\Environment\Environment($config);
$environment->addExtension(new \League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension());
$environment->addExtension(new \League\CommonMark\Extension\Autolink\AutolinkExtension());
$environment->addExtension(new \League\CommonMark\Extension\Strikethrough\StrikethroughExtension());
$environment->addExtension(new \League\CommonMark\Extension\Table\TableExtension());
$environment->addExtension(new \League\CommonMark\Extension\TaskList\TaskListExtension());
// Create converter
$converter = new \League\CommonMark\MarkdownConverter($environment);
// Convert to HTML
$body = $converter->convert($contentWithoutMeta)->getContent();
// Extract title from metadata or first H1
$title = $metadata['title'] ?? '';
if (empty($title) && preg_match('/^#\s+(.+)$/m', $contentWithoutMeta, $matches)) {
$title = trim($matches[1]);
}
// Set special title for guide
$result['title'] = $this->t('manual') . ' - CodePress CMS';
$title = $this->t('manual') . ' - CodePress CMS';
return $result;
return [
'title' => $title,
'content' => $body,
'metadata' => $metadata,
'layout' => $metadata['layout'] ?? 'content'
];
}
/**
@@ -912,12 +1039,21 @@ class CodePressCMS {
// Get homepage title
$homepageTitle = $this->getHomepageTitle();
// Get sidebar content from plugins
$sidebarContent = $this->pluginManager->getSidebarContent();
// Get layout from page metadata
$layout = $page['layout'] ?? 'sidebar-content';
// Prepare template data
$templateData = [
'site_title' => $this->config['site_title'],
'page_title' => htmlspecialchars($page['title']),
'content' => $page['content'],
'sidebar_content' => $sidebarContent,
'layout' => $layout,
'page_metadata' => $page['metadata'] ?? [],
'search_query' => isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '',
'menu' => $this->renderMenu($menu),
'breadcrumb' => $breadcrumb,
@@ -930,7 +1066,7 @@ class CodePressCMS {
'lang_switch_url' => isset($_GET['guide']) ? '&guide' : '&page=' . $this->config['default_page'],
'author_name' => $this->config['author']['name'] ?? 'CodePress Developer',
'author_website' => $this->config['author']['website'] ?? '#',
'author_git' => $this->config['author']['git'] ?? '#',
'author_git' => 'https://git.noorlander.info/E.Noorlander',
'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',
'cms_version' => isset($this->config['version_info']) ? 'v' . $this->config['version_info']['version'] : '',
@@ -939,6 +1075,8 @@ class CodePressCMS {
'header_font_color' => $this->config['theme']['header_font_color'] ?? '#ffffff',
'navigation_color' => $this->config['theme']['navigation_color'] ?? '#f8f9fa',
'navigation_font_color' => $this->config['theme']['navigation_font_color'] ?? '#000000',
'sidebar_background' => $this->config['theme']['sidebar_background'] ?? '#f8f9fa',
'sidebar_border' => $this->config['theme']['sidebar_border'] ?? '#dee2e6',
// Language
'current_lang' => $this->currentLanguage,
'current_lang_upper' => strtoupper($this->currentLanguage),
@@ -967,17 +1105,20 @@ class CodePressCMS {
't_page_not_found' => $this->t('page_not_found'),
't_page_not_found_text' => $this->t('page_not_found_text'),
't_mappen' => $this->t('mappen'),
't_paginas' => $this->t('paginas')
't_paginas' => $this->t('paginas'),
't_author_website' => $this->t('author_website'),
't_author_git' => $this->t('author_git')
];
// File info for footer
if (isset($page['file_info'])) {
$templateData['file_info'] = $this->t('created') . ': ' . htmlspecialchars($page['file_info']['created']) .
' | ' . $this->t('modified') . ': ' . htmlspecialchars($page['file_info']['modified']);
$templateData['file_info_block'] = '<span class="file-details"> | ' . $templateData['file_info'] . '</span>';
$templateData['created'] = htmlspecialchars($page['file_info']['created']);
$templateData['modified'] = htmlspecialchars($page['file_info']['modified']);
$templateData['file_info_block'] = true;
} else {
$templateData['file_info'] = '';
$templateData['file_info_block'] = '';
$templateData['created'] = '';
$templateData['modified'] = '';
$templateData['file_info_block'] = false;
}
@@ -1025,7 +1166,7 @@ class CodePressCMS {
*
* @return string Breadcrumb HTML
*/
private function generateBreadcrumb() {
public function generateBreadcrumb() {
if (isset($_GET['search'])) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '&lang=' . $this->currentLanguage . '"></a></li><li class="breadcrumb-item"> > </li><li class="breadcrumb-item active">' . $this->t('search') . '</li></ol></nav>';
}

View File

@@ -37,6 +37,16 @@ class SimpleTemplate {
// Handle partial includes first ({{>partial}})
$template = preg_replace_callback('/{{>([^}]+)}}/', [$this, 'replacePartial'], $template);
// Handle equal conditionals first
$template = preg_replace_callback('/{{#equal\s+(\w+)\s+["\']([^"\']+)["\']}}(.*?){{\/equal}}/s', function($matches) {
$key = $matches[1];
$expectedValue = $matches[2];
$content = $matches[3];
$actualValue = $this->data[$key] ?? '';
return ($actualValue === $expectedValue) ? $content : '';
}, $template);
// Handle conditional blocks
foreach ($this->data as $key => $value) {
if (is_array($value)) {

View File

@@ -0,0 +1,216 @@
<?php
class CMSAPI
{
private CodePressCMS $cms;
public function __construct(CodePressCMS $cms)
{
$this->cms = $cms;
}
/**
* Get current page information
*/
public function getCurrentPage(): array
{
return $this->cms->getPage();
}
/**
* Get current page title
*/
public function getCurrentPageTitle(): string
{
$page = $this->cms->getPage();
return $page['title'] ?? '';
}
/**
* Get current page content
*/
public function getCurrentPageContent(): string
{
$page = $this->cms->getPage();
return $page['content'] ?? '';
}
/**
* Get current page URL
*/
public function getCurrentPageUrl(): string
{
$page = $_GET['page'] ?? $this->cms->config['default_page'];
$lang = $_GET['lang'] ?? $this->cms->config['language']['default'] ?? 'nl';
return "?page={$page}&lang={$lang}";
}
/**
* Get menu structure
*/
public function getMenu(): array
{
return $this->cms->getMenu();
}
/**
* Get configuration value
*/
public function getConfig(string $key, $default = null)
{
$keys = explode('.', $key);
$value = $this->cms->config;
foreach ($keys as $k) {
if (!isset($value[$k])) {
return $default;
}
$value = $value[$k];
}
return $value;
}
/**
* Get translation
*/
public function translate(string $key): string
{
return $this->cms->t($key);
}
/**
* Get current language
*/
public function getCurrentLanguage(): string
{
return $this->cms->currentLanguage;
}
/**
* Check if user is on homepage
*/
public function isHomepage(): bool
{
$defaultPage = $this->cms->config['default_page'] ?? 'index';
$currentPage = $_GET['page'] ?? $defaultPage;
return $currentPage === $defaultPage;
}
/**
* Get file info for current page
*/
public function getCurrentPageFileInfo(): ?array
{
$page = $this->cms->getPage();
return $page['file_info'] ?? null;
}
/**
* Get breadcrumb data
*/
public function getBreadcrumb(): string
{
return $this->cms->generateBreadcrumb();
}
/**
* Check if content directory has content
*/
public function hasContent(): bool
{
return !$this->cms->isContentDirEmpty();
}
/**
* Get search results if searching
*/
public function getSearchResults(): array
{
if (isset($_GET['search'])) {
return $this->cms->searchResults;
}
return [];
}
/**
* Check if currently searching
*/
public function isSearching(): bool
{
return isset($_GET['search']);
}
/**
* Get available languages
*/
public function getAvailableLanguages(): array
{
return $this->cms->getAvailableLanguages();
}
/**
* Create URL for page
*/
public function createUrl(string $page, ?string $lang = null): string
{
$lang = $lang ?? $this->getCurrentLanguage();
return "?page={$page}&lang={$lang}";
}
/**
* Execute PHP file and capture output
*/
public function executePhpFile(string $filePath): string
{
if (!file_exists($filePath)) {
return '';
}
ob_start();
include $filePath;
return ob_get_clean();
}
/**
* Get content from PHP/HTML/Markdown file
*/
public function getFileContent(string $filePath): string
{
if (!file_exists($filePath)) {
return '';
}
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
switch ($extension) {
case 'php':
return $this->executePhpFile($filePath);
case 'md':
$content = file_get_contents($filePath);
$result = $this->cms->parseMarkdown($content, $filePath);
return $result['content'] ?? '';
case 'html':
return file_get_contents($filePath);
default:
return file_get_contents($filePath);
}
}
/**
* Check if file exists in content directory
*/
public function contentFileExists(string $filename): bool
{
$contentDir = $this->cms->config['content_dir'];
return file_exists($contentDir . '/' . $filename);
}
/**
* Get all pages with their metadata
*/
public function getAllPages(): array
{
return $this->cms->getAllPageTitles();
}
}

View File

@@ -0,0 +1,77 @@
<?php
class PluginManager
{
private array $plugins = [];
private string $pluginsPath;
private ?CMSAPI $api = null;
public function __construct(string $pluginsPath)
{
$this->pluginsPath = $pluginsPath;
$this->loadPlugins();
}
public function setAPI(CMSAPI $api): void
{
$this->api = $api;
// Inject API into all plugins that have setAPI method
foreach ($this->plugins as $plugin) {
if (method_exists($plugin, 'setAPI')) {
$plugin->setAPI($api);
}
}
}
private function loadPlugins(): void
{
if (!is_dir($this->pluginsPath)) {
return;
}
$pluginDirs = glob($this->pluginsPath . '/*', GLOB_ONLYDIR);
foreach ($pluginDirs as $pluginDir) {
$pluginName = basename($pluginDir);
$pluginFile = $pluginDir . '/' . $pluginName . '.php';
if (file_exists($pluginFile)) {
require_once $pluginFile;
$className = $pluginName;
if (class_exists($className)) {
$this->plugins[$pluginName] = new $className();
// Inject API if already available
if ($this->api && method_exists($this->plugins[$pluginName], 'setAPI')) {
$this->plugins[$pluginName]->setAPI($this->api);
}
}
}
}
}
public function getPlugin(string $name): ?object
{
return $this->plugins[$name] ?? null;
}
public function getAllPlugins(): array
{
return $this->plugins;
}
public function getSidebarContent(): string
{
$sidebarContent = '';
foreach ($this->plugins as $plugin) {
if (method_exists($plugin, 'getSidebarContent')) {
$sidebarContent .= $plugin->getSidebarContent();
}
}
return $sidebarContent;
}
}