Remove language-specific content handling

Refactor to use plugin system for language support

Remove hardcoded language features from core

Move language handling to plugin system

Improve content file discovery

Simplify context creation

Add plugin system documentation

Implement hook system for extensibility

Add template variable hook

Add context storage for plugins

Improve error handling

Refactor rendering logic

Improve list view sorting

Add support for custom list templates

Improve metadata handling

Add plugin system reference documentation
This commit is contained in:
Ruben 2025-11-25 20:19:12 +01:00
parent 24ee209e17
commit a205f2cbd7
8 changed files with 524 additions and 315 deletions

View file

@ -1,7 +1,7 @@
<?php
// Find all content files in a directory (supporting language variants)
function findAllContentFiles(string $dir, string $lang, string $defaultLang, array $availableLangs): array {
// Find all content files in a directory
function findAllContentFiles(string $dir): array {
if (!is_dir($dir)) return [];
$files = scandir($dir) ?: [];
@ -16,185 +16,96 @@ function findAllContentFiles(string $dir, string $lang, string $defaultLang, arr
$filePath = "$dir/$file";
if (!is_file($filePath)) continue;
// Parse filename to check for language variant
$parts = explode('.', $file);
// Check if this is a language-specific file
if (count($parts) >= 3) {
// Pattern: name.lang.ext
$fileLang = $parts[count($parts) - 2];
if (in_array($fileLang, $availableLangs)) {
// Only include if it matches current language
if ($fileLang === $lang) {
$contentFiles[] = [
'path' => $filePath,
'name' => $file,
'sort_key' => $parts[0]
];
}
continue;
}
}
// Default files (no language suffix) - include if no language-specific version exists
$baseName = $parts[0];
$hasLangVersion = false;
if ($lang !== $defaultLang) {
// Check if language-specific version exists
foreach (CONTENT_EXTENSIONS as $checkExt) {
if (file_exists("$dir/$baseName.$lang.$checkExt")) {
$hasLangVersion = true;
break;
}
}
}
if (!$hasLangVersion) {
$contentFiles[] = [
'path' => $filePath,
'name' => $file,
'sort_key' => $baseName
];
}
$contentFiles[] = [
'path' => $filePath,
'name' => $file,
'ext' => $ext
];
}
// Sort by filename (alphanumerical)
usort($contentFiles, fn($a, $b) => strnatcmp($a['sort_key'], $b['sort_key']));
// Let plugins filter content files (e.g., by language)
$contentFiles = Hooks::apply(Hook::PROCESS_CONTENT, $contentFiles, $dir);
// Sort by filename
usort($contentFiles, fn($a, $b) => strnatcmp($a['name'], $b['name']));
return array_column($contentFiles, 'path');
}
function resolveTranslatedPath(Context $ctx, string $requestPath): string {
// If default language, no translation needed
if ($ctx->currentLang === $ctx->defaultLang) {
return $requestPath;
}
$parts = explode('/', trim($requestPath, '/'));
$resolvedParts = [];
$currentPath = $ctx->contentDir;
foreach ($parts as $segment) {
if (empty($segment)) continue;
// Check all subdirectories for slug matches
$found = false;
if (is_dir($currentPath)) {
$subdirs = getSubdirectories($currentPath);
foreach ($subdirs as $dir) {
$metadata = loadMetadata("$currentPath/$dir", $ctx->currentLang, $ctx->defaultLang);
if ($metadata && isset($metadata['slug']) && $metadata['slug'] === $segment) {
$resolvedParts[] = $dir;
$currentPath .= "/$dir";
$found = true;
break;
}
}
}
// If no slug match, use segment as-is
if (!$found) {
$resolvedParts[] = $segment;
$currentPath .= "/$segment";
}
}
return implode('/', $resolvedParts);
}
function parseRequestPath(Context $ctx): array {
// Resolve translated slugs to actual directory names
$resolvedPath = resolveTranslatedPath($ctx, $ctx->requestPath);
$contentPath = rtrim($ctx->contentDir, '/') . '/' . ltrim($resolvedPath, '/');
if (is_file($contentPath)) {
return ['type' => 'file', 'path' => realpath($contentPath)];
$requestPath = $ctx->requestPath;
if (empty($requestPath)) {
return ['type' => 'frontpage', 'path' => $ctx->contentDir];
}
$contentPath = $ctx->contentDir . '/' . $requestPath;
// Check if it's a directory
if (is_dir($contentPath)) {
// Check if directory has subdirectories (PHP 8.4: cleaner with array_any later)
$hasSubdirs = !empty(getSubdirectories($contentPath));
// If directory has subdirectories, it's an article-type folder (list view)
if ($hasSubdirs) {
return ['type' => 'directory', 'path' => realpath($contentPath)];
$items = scandir($contentPath) ?: [];
$subdirs = array_filter($items, fn($item) =>
$item !== '.' && $item !== '..' && is_dir("$contentPath/$item")
);
if (!empty($subdirs)) {
return ['type' => 'list', 'path' => $contentPath];
} else {
return ['type' => 'page', 'path' => $contentPath];
}
// No subdirectories - it's a page-type folder
// Find all content files in this directory
$contentFiles = findAllContentFiles($contentPath, $ctx->currentLang, $ctx->defaultLang, $ctx->availableLangs);
if (!empty($contentFiles)) {
return ['type' => 'page', 'path' => realpath($contentPath), 'files' => $contentFiles, 'needsSlash' => !$ctx->hasTrailingSlash];
}
// No content files found
return ['type' => 'directory', 'path' => realpath($contentPath)];
}
return ['type' => 'not_found', 'path' => $contentPath];
}
function loadMetadata(string $dirPath, string $lang, string $defaultLang): ?array {
function loadMetadata(string $dirPath): ?array {
$metadataFile = "$dirPath/metadata.ini";
if (!file_exists($metadataFile)) return null;
$metadata = parse_ini_file($metadataFile, true);
if (!$metadata) return null;
// Extract base metadata (non-section values)
// Get base metadata (non-array values)
$baseMetadata = array_filter($metadata, fn($key) => !is_array($metadata[$key]), ARRAY_FILTER_USE_KEY);
// If current language is not default, merge language-specific overrides
if ($lang !== $defaultLang && isset($metadata[$lang]) && is_array($metadata[$lang])) {
$baseMetadata = array_merge($baseMetadata, $metadata[$lang]);
}
return $baseMetadata ?: null;
// Store full metadata for plugins to access
$baseMetadata['_raw'] = $metadata;
// Let plugins modify metadata (e.g., merge language sections)
return Hooks::apply(Hook::PROCESS_CONTENT, $baseMetadata, $dirPath, 'metadata');
}
function buildNavigation(Context $ctx): array {
$items = scandir($ctx->contentDir) ?: [];
$navItems = [];
$items = getSubdirectories($ctx->contentDir);
foreach ($items as $item) {
if ($item === '.' || $item === '..' || !is_dir($ctx->contentDir . "/$item")) continue;
$itemPath = "{$ctx->contentDir}/$item";
$metadata = loadMetadata($itemPath, $ctx->currentLang, $ctx->defaultLang);
// Check if this item should be in menu
if (!$metadata || empty($metadata['menu'])) {
$metadata = loadMetadata($itemPath);
// Only include if explicitly marked as menu item
// parse_ini_file returns boolean true as 1, false as empty string, and "true"/"false" as strings
if (!$metadata || !isset($metadata['menu']) || !$metadata['menu']) {
continue;
}
// Check if content exists for current language
if ($ctx->currentLang !== $ctx->defaultLang && shouldHideUntranslated()) {
$hasLangContent = hasLanguageContent($itemPath, $ctx->currentLang, CONTENT_EXTENSIONS);
$hasLangMetadata = hasLanguageMetadata($itemPath, $ctx->currentLang);
if (!$hasLangContent && !$hasLangMetadata) continue;
}
// Extract title and build URL
$title = $metadata['title'] ?? extractTitle($itemPath, $ctx->currentLang, $ctx->defaultLang) ?? ucfirst($item);
// Use translated slug if available
$urlSlug = ($ctx->currentLang !== $ctx->defaultLang && $metadata && isset($metadata['slug']))
? $metadata['slug']
: $item;
// Extract title
$title = $metadata['title'] ?? extractTitle($itemPath) ?? ucfirst($item);
// Use slug if available, otherwise use folder name
$urlSlug = ($metadata && isset($metadata['slug'])) ? $metadata['slug'] : $item;
$navItems[] = [
'title' => $title,
'url' => $ctx->langPrefix . '/' . urlencode($urlSlug) . '/',
'url' => '/' . urlencode($urlSlug) . '/',
'order' => (int)($metadata['menu_order'] ?? 999)
];
}
// Sort by menu_order
usort($navItems, fn($a, $b) => $a['order'] <=> $b['order']);
return $navItems;
}